(MVC 5 előzetessel)
Minden szó előtt szeretnék köszönetet mondani azoknak, akik segítségemre voltak a könyv
elkészítéséhez.
A feleségemnek a sok türelméért és azért, hogy segített átnézni ezt a nem könnyű olvasmányt.
A fiamnak, ifj. Regius Kornélnak, aki sok-sok hibát felfedezve hozzájárult a minőség javításához.
Balássy Györgynek és Reiter Istvánnak a tanácsokért. Nagyon jó ötleteket és javaslatokat kaptam tőlük
a könyv felépítéséhez és tartalmához.
UTÓSZÓ................................................................................................................................................... 12-360
1.1 Felvezető - Ajánló 1-6
1. Felvezető
1.1. Ajánló
Azok számára írtam ezt a könyvet, akik még nem ismerik ezt a keretrendszert, vagy ismerik, de úgy
érzik még nem eléggé (így magamnak is írtam :-). Lehet, hogy csak szeretnének egy átfogó képet kapni,
vagy csak magyarul szeretnének olvasni egy egyébként angolul jól dokumentált rendszerről. Az elmúlt
évek jó és rossz tapasztalata, a hozzám intézett kérdések és az ezek mögött rejlő ismeret hiányosságok
indítottak arra, hogy egy jórészt gyakorlati szemléletű könyvet készítsek. Egy további oknak említeném,
hogy a net tele van olyan ajánlásokkal, best-practice-ekkel, amik idejétmúltak és már nem érvényesek
az MVC 4 verzióval kapcsolatban sem. Lesznek benne részek, amik túl alapszintűnek fognak tűnni, de
a napi munkában jelentkező kérdések azt sugallták számomra, hogy még ezek sem tisztázottak
rendesen, még néhány MVC környezetben fejlesztő számára sem. Minden bizonnyal lesznek nehezebb
részek is, amik a későbbi fejlesztési munkák során valószínűleg előbukkanó problémákat járják körbe.
Az MVC framework belső felépítése annyira rugalmas, hogy kevés olyan webes környezetben
megjelenő igényt ismerek, amire ne lehetne legalább két jó megoldást is adni az MVC segítségével,
többek között ezért szeretnék egy nagyon fontos dolgot tisztázni, amolyan garanciális feltételként:
A könyvben szereplő megoldások, tippek nem biztos, hogy a legmegfelelőbbek minden helyzetre,
tehát nyugodtan kételkedjen benne az olvasó. Nézzen utána, ha valami felbosszantja, ha
butaságnak tartja. Járja be az utat, ami a tökéletes megoldáshoz vezet, és utána ossza meg, hadd
okuljanak belőle mások is, és én is. Mert nincs az a szoftver és ismeret, amit ne lehetne feljavítani
és bővíteni. Ilyen a természetük.
A könyv tartalmi célja, hogy bemutassa az ASP.NET MVC-t, mint fejlesztési keretrendszert az alapoktól
kezdve, a jelenleg kiadott 4-es verzión keresztül. Az MVC 5-ös verziója ebben a pillanatban preview
állapotban van, tehát biztosat nem lehet róla mondani, de hogy legyen képünk arról, hogy mire
számíthatunk, a fejezetek közé beékeltem az 5-ös verzió meglévő és várható újdonságait. Ezeket a
felirattal láttam el. Ezek a szekciók még képlékenyek.
Mivel a téma még bevezető szinten is nagyon szerteágazó, próbáltam a fókuszt az MVC-n tartani, ezért
az MVC-hez egyébként jól illeszkedő technológiákról csak érintőlegesen lesz szó. A könyvben található
példák - ritka kivétellel - csak memóriában tárolt adatokat fognak használni.
A könyv címében levő ’+’, arra a tapasztalati és gyakorlati plusz kiegészítésre utal, amit a nyers
alaptechnológiai ismeretek mellé tettem. Régebben a játékok örökélet és egyéb cheat módokat
elérhetővé tevő "javító" programok neve után szerepelt a +, ++, +++ jel. Utalva arra, hogy így majd
könnyebb lesz végigjátszani. Reményem szerint a könyv segítségével könnyebb lesz használatba venni
az MVC keretrendszert.
1.2 Felvezető - A szerzőről 1-7
1.2. A szerzőről
Az azóta eltelt időben elektronikával, 8-32 bites CPU-k hardverközeli programozásával, adatbázis- és
szolgáltatáshátterű alkalmazásfejlesztéssel, webfejlesztéssel foglalkoztam. Írtam programokat
mindenféle CPU-ra, mikrokontrollere Assembly-ben, C-ben. Ügyviteli, munkaügyi kisebb-nagyobb
alkalmazást Visual Foxpro-ban, MFC+C -ben, Delphi-ben. Természetesen .Net környezetben is jó sokat.
Jó darabig idegenkedtem a webfejlesztéstől. Majd egyszer mégis neki kellett állnom PHP alapú CMS
rendszereket készíteni, integrálni, mert nem volt más az akkori cégemnél, aki meg tudta volna csinálni.
Ekkor a HTML ismereteim még elég sekélyesek voltak, de egy évre rá HTML+CSS+Webszerver
tematikájú kurzusokat tartottam tanfolyamokon. Annyira megszerettem ezt a világot, hogy hamarosan
futószalagszerűen kezdtem gyártani a web site-okat, akkor még leginkább Drupal/PHP 2 alapokon.
Majd mikor megjelent az ASP.NET MVC1.0 béta azonnal lecsaptam rá. Azóta egy bélyeggyűjtő
hóbortosságával követem a változásait, fejlődését. Nem tudom megmagyarázni, de valamiért a benne
levő architektúrát, a kódolási stílust, a képlékenységét, a fejlesztési szabadságot nagyon jónak érzem.
Emlékeztet arra, amit 13 éves koromban tapasztaltam az orosz számológép után. Részben ez adott
ihletet arra, hogy ezt a könyvet megírjam. Hasonló elszántságot és kitartást kívánva ajánlom
tanulmányozásra a következő fejezeteket. Remélem, hasznát veszi a kedves olvasó.
http://www.cornelius.hu
http://blog.cornelius.hu
https://www.facebook.com/kornel.regius
http://hu.linkedin.com/in/regiuskornel
1
http://ht.homeserver.hu/
2
Ez egy nagyon innovatív, közösségi fejlesztésű, moduláris, open-source MVC platform
1.3 Felvezető - Hasznos dolgok 1-8
A könyvben szereplő példakódokat szintén egyben le lehet tölteni. Ez egy egyszerű kétprojektes
kódgyűjtemény. Nem alkot komplett összefüggő mintaalkalmazást, viszont számos helyen kiegészíti a
könyv tartalmát, mert ami a könyvben csak kivonatos kód formájában szerepel, annak teljes
terjedelmű változata itt megtalálható. Legtöbb esetben fapados „megvalósíthatósági tanulmányok”
(POC) és csak arra jók, hogy break pointkokkal megállva tanulmányozhassuk a valódi működést, és
hogy ne kelljen begépelni még egyszer. A példakódok között leginkább a modellosztályokra jellemző,
hogy újrafelhasználásra kerültek és a téma kifejtése során átalakultak a kiinduló állapothoz képest.
Emiatt előfordul, hogy a kódblokkok és attribútumok ki vannak kommentezve (de törölve nem). Ilyen
esetben értelemszerűen vissza kell alakítani olyanra, amit az aktuális téma leír, igényel.
A példakódok megértéséhez szükség lesz a C# újabb lehetőségeinek alapos ismeretére is, mivel az MVC
framework is erősen ezekre épít. Ha az olyan nyelvi sajátosságok ismerősen csengenek, mint ’partial
class’, ’nullable típus’, ’opcionális metódus paraméter’, ’anonymous metódus', 'dinamikus típus’,
’lambda expression’, akkor azt hiszem nem lesz gond a példakódok megértésével.
Az MVC hivatalos oldala a http://www.asp.net/mvc, ahol sok hasznos, angol nyelvű oktatóanyag
lelhető fel szöveges és oktató videó változatban.
Reiter István jóvoltából egy további hasznos gyakorlati MVC bemutató (step-by-step) fordítását lehet
elérni magyar nyelven ezen a linken: http://reiteristvan.wordpress.com/2012/03/06/asp-net-mvc-
egyszeru-webshop-lpsrol-lpsre-fordts-123-oldal/
1.4 Felvezető - A rövidítésekről, nevekről és jelekről 1-9
A könyvben a bevált hétköznapi terminológiát követem, ami lehet, hogy egyes helyeken
magyartalannak tűnhet. Azokat a szavakat, amelyeknek nincs jó magyar megfelelője, angol
kifejezéssel fogom leírni. Sokszor azt is, aminek van. Példának említem a request szót, aminek megvan
a magyar megfelelője (kérés, lekérés), de azzal, hogy mégis az angol szót használom, érzékeltetni
igyekszem, hogy adott helyzetben egy szigorúan technikai protokoll szintű akcióról és adatról van szó,
és nem valami humán kérvényről. Fejlesztői körökben számos olyan szó van napi használatban, amik
ezen stíluson is átlépnek. Csapatmunkában a ’rendereli’, ’lebildelem’, ’becsekkoltam’, szavak
használata is azt mutatja, hogy ilyen körökben a gyors és hatékony munka érdekében a magyar nyelv
bővítése gyakorlati okoknál fogva folyamatos. Igyekszem az ilyen szerkezeteket kerülni, de ha a kedves
olvasó mégis megütközik ezen így nem tehetek mást, előre is elnézést kérek.
Elkerülhetetlen, ezért lesznek betűszavak is, különösen olyanok, amelyek beváltak a hétköznapokban.
Erre példa, hogy a Cascading Style Sheets megnevezét legritkább esetben láthatjuk egy szakkönyvben,
helyette a fájlnév kiterjesztésének is használt ’CSS’ betűszó az elterjedt. Ugyan így van ez a JavaScript-
el is, amire JS–ként fogok hivatkozni a legtöbb helyen. A könyv fő témája az ASP.NET MVC 4 és az 5, de
általában csak MVC-t írok helyette. Ahol ASP.NET+MVC szerepel, ott azt szeretném hangsúlyozni, hogy
az adott képességet nem is annyira az MVC keretrendszer biztosítja, hanem a inkább mélyben dolgozó,
alap ASP.NET motor. Vettem a bátorságot és a könyvben a HTML és az AJAX szerepel ilyen formában
is: Html, Ajax. Majd látni fogjuk, de van két ilyen nevű property, amik a HTML előállítást segítő
osztályokat hordozzák. Ezek az un. helperek. Így a "Html helper" és "Ajax helper" ezekre való
hivatkozás.
3
http://tfs.visualstudio.com/ - Online ingyenes TFS. Egy verziókövető rendszer, csak ajánlani tudom.
2.1 Bevezetés - A tendenciák áttekintése 1-10
2. Bevezetés
Az MVC első verziójának megjelenése óta jelentős változások mentek végbe a webes fejlesztési
világban. Határozottan kiemelt fontosságú lett a kliens oldali interaktivitás, a felhasználói élmény
fokozása, új platformok kiszolgálása és az MVC keretrendszer ebben egyáltalán nincs lemaradva.
Ahhoz, hogy pozícionálni tudjuk az ASP.NET MVC technológiát érdemes lesz egy visszatekintéssel és
általános megközelítéssel kezdeni. Sokat látott programozókban egy kimondott vagy kimondatlan
kérdés szokott megfogalmazódni, ha egy eddig nem ismert betűszót lát. „Miért kell, megint egy új
technológia?”. De könnyen el tudom képzelni, hogy a kérdést egy diploma előtt álló kolléga is ugyan
ilyen természetességgel tudja feltenni. Mindenesetre egyáltalán nem új dologról van szó. Az ASP.NET
MVC 1.0 évekkel ezelőtt elérhetővé vált (~2009) a .NET-es világ számára. Az MVC architektúra egy
programozási környezet, független alkalmazástervezési minta, így más fejlesztési nyelvekben és
környezetekben (Java, PHP, Ruby) is régóta hasznosítják az előnyeit az ottani keretrendszerek. Nagyon
jellemző a használata a webes alkalmazások kialakításánál, ahol a fejlesztés eleve több programozási
platformon folyik párhuzamosan. Ez a bevezető fejezet azokat a sarokpontokat gyűjti össze, ami az
induláshoz szükséges lehet.
Igencsak megváltoztak az igények az ASP / ASP.NET / PHP és más dinamikus HTML oldalgeneráló
eszközök megjelenése óta. Az általánosan megnövekedett sávszélesség lehetővé teszi, hogy ne kelljen
1-10 kilóbájtokban számolni a képek és szkriptek méretét, mint mondjuk 10 évvel ezelőtt, amikor 1
megabájt letöltése hosszú percekbe telt. Ma nem ritka, hogy egy üzleti alkalmazás oldala 1-2 megabájt
adatot küld át a böngészőnek csak azért, hogy az oldal kezdeti állapota megjelenjen. Még 2-3 kattintás
és +1 megabájt töltődött le a gépünkre. Az unalmas HTML beviteli mezőket ötletes, mozgatható képi
elemek váltják fel. Dinamikusan töltődő (autocomplete) legördülő listák, az oldal letöltése után a
felhasználóval interakcióban kapják meg az elemeiket. Előtérbe kerültek az adatkeresést segítő felületi
elemek, szűrők. A ma már magától értetődő dinamikus menütartalom mellett új, felbontás érzékeny
elrendezési modellek jelentek meg. Nagyon fontossá vált az esztétika és az ergonómia, a felhasználói
élmény pszichológiai vetülete. Néhány éve még az volt a kérdés a specifikáció összeállításkor, hogy a
weboldalak 1024x768 vagy 800x600-ra méretre legyenek optimalizálva. Esetleg Internet Explorerre
vagy Firefoxra? Ma egy ilyen kérdés esetén a megrendelő jogosan kérdőjelezheti meg a szakmai
tudásunkat, hisz teljesen evidens, hogy jól kell működnie a 600x480-as felbontású mobileszközön,
számunkra ismeretlen böngészővel is. De ha ez nem is lesz szempont, majd lesz az, hogy az Android
alapú kütyüjére írt programot azonos adattartalommal tudjuk kiszolgálni, mint amit a 2 x Full-HD-s
tabletjén a Safari böngészője megjelenít. Nem is olyan régen első hallásra meglepődtem, mikor egy
nagyvállalat intranet alkalmazásánál a megrendelő megjelenési elképzelése mindössze annyi volt, hogy
az új rendszerük legyen olyan, mint a Facebook. Nos, ez pedig nagyon lényegre törően fogalmazza meg
azt, hogy hiába fejlesztik a Facebookot jóval többen (~3000) mint a mi fejlesztő csapatunk állománya,
a mérce magasra van téve. Az adatoknak ma nem kis szigetekként kell elérhetőeknek lennie, hanem
együtt kell működniük más rendszerek adataival, tartalmi hálózatot alkotva. Lehet, hogy az oldalunkon
megjelenő táblázat egyik oszlopa egy teljesen más, tőlünk független szervertől származik, míg egy
másik oszlop szintén más szervertől. És tudnék még fejtegetni és filozofálni, hogy mekkorát fordult a
világ, de talán érzékelhető, hogy mások az igények ma, mint amikor az ASP.NET első kiadása megjelent.
2.2 Bevezetés - A webes alkalmazásokról általában 1-11
Szükséges lehet néhány fogalom tisztázása, ismétlése, stb. Ha valami nem ismerős még ezek közül az
alapfogalmak közül, akkor érdemes alaposabban utána nézni és csak utána továbbolvasni ezt, mert jó
alapok nélkül nem sok hasznát lehet venni ennek a könyvnek. Szinte csak tőmondatokban és
felsorolásszerűen néhány fontos pont az URL értelmezéséhez:
Az URL alapesetben, a webkiszolgáló fájlrendszerében levő fájlt határoz meg, hasonlóan ehhez:
Persze ez így kicsit erőltetett. Egy sokkal kézzelfoghatóbb alkalmazása, amit a ’friendly URL’ névvel
szoktak illetni, és ami azt az eredeti ősi célt szolgálja, hogy az URL emberközeli és könnyen olvasható
legyen.
Talán nem szorul magyarázatra, hogy a második miért kellemesebb a szemnek és a SEO 4
szempontoknak sem árt, ha így néz ki. A friendly URL-hez még hozzá tartozik, hogy ebből nagyon
könnyű a webkiszolgáló számára érthető paraméteres (query stringes) URL-t képezni. Ennek a
módszernek a neve: URL rewrite.
Hogyha ezt tovább gondoljuk és hozzáadjuk az URL rewrite képességet, amivel még a fájlnév
hivatkozást sem kell elvárni az URL-ben (nem kell index.aspx-re, index.php-ra referálni), akkor a
webszerver alapműködését alaposan át tudjuk formálni. Azt is megtehetjük, hogy a nyers
fájlkiszolgálás lesz a ritkább eset és leginkább dinamikusan generáljuk az oldalakat sablonok alapján.
Ezt csinálja lényegében az MVC is. A HTML oldalakat dinamikusan állítja össze és az oldal által igényelt
4
Search Engine Optimization – Az oldalunk optimalizálása, hogy a tartalom jól értelmezhető legyen a kereső
motorok számára, Google, Yahoo, stb.
2.3 Bevezetés - Böngésző – szerver interakció 1-12
további képeket, CSS fájlokat pedig statikus fájlként szolgálja ki, hagyományos fájlnév-erőforrás
leképzéssel.
Érdemes azt is látni, hogy egy oldal letöltése nem áll meg a megcímzett HTML tartalom letöltése után
- ami jellemzően nem csak 50-200Kbyte nagyságrendű adatot jelent – hanem folytatódik a letöltés
tovább. Letöltődnek a CSS, JS fájlok és a képek. A szkriptekben levő kódok aktiválódnak és további
képeket, HTML szakaszokat töltenek le. Mire az 1db oldalunk tartalma megjelenik, már lezajlott
nagyságrendileg 5-100 további HTTP kérés (request), amelyek összességében megabájtban kifejezhető
adatmennyiséget jelentenek. Erre a szerverünknek van kb. 3 másodperce. Ez utóbbi adat egy ajánlás,
mert a 3. másodperc várakozás után a látogatóink szubjektív megítélése rohamosan billen át a negatív
tartományba. A 10. másodpercnél pedig az oldalunkat tudattalanul is leírják. Ezek miatt fontos olyan
rendszerekben gondolkodni, ami ezt az igénycsoportot jól ki tudja elégíteni:
Az oldalnak sok olyan képi elemet kell tartalmaznia, ami miatt vonzó lesz a tekintet számára
A lehető leginteraktívabb legyen, ami sok és összetett böngészőben futó javascriptet jelent.
Minden álljon készen 2-3 másodperc alatt.
Legyen biztonságos
Legyen könnyen programozható, bővíthető.
Véleményem szerint az ASP.NET MVC jó esélyt ad, hogy hatékonyan meg tudjunk felelni ezeknek a
kihívásoknak.
A kliens (böngésző) néhány ’ igén ’, METHOD-on keresztül intézi a kéréseit. Ezek a parancsok lefedik az
általános adatkezelés (CRUD) igényeit.
A két legfontosabb:
- A GET, amivel egy oldalt tudunk elkérni a szervertől az URL-en keresztül. A GET-nek csak két
paramétere van: az URL a paramétereivel és a verzió szám.
- A másik metódus a POST, amivel leggyakrabban a böngészőben levő kitöltött adatlap (form)
mezőinek az értékét tudjuk visszaküldeni feldolgozás céljából, a szervernek. Nyilvánvalóan
ennek is van URL-je, de az nem szokott tartalmazni paramétereket (de nem kizárt, ahogy az
sem hogy egyes böngészők ezt nem támogatják). A form input mezői (a neve és tartalma) a
HTTP csomagban vannak. A post csomagot nem csak HTML formmal lehet előállítani, hanem
JS kóddal is.
amikor GET-el lekért oldalba ágyazott form adatait visszaküldjük egy POST requesttel. Ez a
terminológia a Web Forms fejlesztéssel kapcsolatban igen képszerű. Mivel a teljes oldal egy komplett
form, ami azonos URL-re lesz visszaküldve, mint ami az oldalt előállította. A Web Forms esetén a GET
és a POST request feldolgozása a Page Load eseményen megy keresztül és ott általában egy elágazást
kell készíteni a feldolgozásban, hogy éppen GET vagy POST(back) szituációban vagyunk. Az MVC
esetében ezt a szituációt szűrők választják ketté így számunkra a tisztán GET és POST esetén induló
metódusok fogják feldolgozni a requestet. Mivel igen gyakori, hogy más URL-re, és szinte biztosan
másik feldolgozó metódushoz érkezik meg a POST csomag, mint ahonnan szármázik, ezért a postback
szó nem teljesen megfelelő MVC esetében, ezért nem is fogom használni.
A HTTP protokoll jelenlegi formájában állapotmentes (stateless), ami azt jelenti, hogy a klienstől
induló kérésre a szerver válaszol és elküldi az URL-ben kért erőforrást (fájlt), és utána így protokoll
szinten nem emlékeznek egymásra. Nincs sorszám, emlékeztető, Id, stb. A szerver - segédeszközök
nélkül - nem tud összefüggést találni az azonos böngészőből, azonos felhasználótól jövő két egymás
utáni kérés között. Szerencsére van egy sessionazonosító cookie, amit az ASP.NET az első válaszához
hozzáfűz, ha az egymás utáni oldallekérések (a requestek) között a felhasználóhoz kötött,
megmaradó adatokat szeretnénk tárolni a szerver oldalon. Ha a kliens a következő kérésébe ezt az
azonosítót szintén belefűzi, akkor a szervernek meg lesz a referenciája az előző kérésre, hisz abba, ő
fűzte bele a saját maga által kiokoskodott számot. Ez a sessionazonosító egy Session példányt
azonosít. Ebben tetszőleges adatot tárolhatunk a szerveren (memóriában, fájlban, adatbázisban).
Bizonyára 1000 érvet fog tudni felhozni egy tapasztalt ASP.NET programozó, hogy az ASP.NET Web
Forms „mindenre elégséges” (amúgy ez egy isteni jelző), hisz évek óta tapasztalja, hogy meg lehet
oldani azzal a platformmal is mindent (ha meg nem, majd alkalmazkodik az ügyfél ). Nézzük meg,
hogy mik azok a problémás részek egy ASP.NET + Web Forms rendszernél!
Az előzőhöz kapcsolódva van egy harmadik szembetűnő dolog is, amit úgy hívnak, hogy
viewstate. Erről is lehet jót és rosszat is mondani, a lényege az, hogy az oldal vezérlőinek az
állapotát tartalmazza. Ma már az ASP.NET Web Forms is úgy szereti, hogyha ezt csak korlátok
között használjuk, ugyanis ebbe bele kerülhet pl. az oldalon található táblázatvezérlő összes
lényegi adata. Ez, és más hasonló okokból kifolyólag ez az adathalmaz, igen méretesre tud
nőni. A jó hír, hogy az MVC-ben nincs viewstate, de ez egyben a rossz hír is, ugyanis manapság
minden böngésző támogatja a többfüles böngészést, és ezt a felhasználók ki is használják.
Emiatt sok esetben előfordul, hogy valamilyen formában szimulálni kell MVC-ben egy
viewstate jellegű viselkedést, hogy a több böngészőablakba lekért oldalak
megkülönböztethetőek legyenek az állapotuk szerint. Kevés az az MVC oldal, amin nincs
legalább egy hidden HTML mező…
Az ASP.NET Web Forms egy HTML formot támogat. Innen kell elindulni és ezzel kell együtt élni.
Az MVC-nél nincs ilyen megkötés, mivel a HTML szabványban sincs. Egy oldalra sok űrlapot is
kitehetünk, a kérdés, hogy a mai AJAX trendek mellett, erre szükség van-e egyáltalán.
A tradicionálisan alkalmazott User Control-ok, Webpartok logikája arra a gyakorlatra épült,
hogy az oldal egésze egymenetben generálódik. Igaz léteznek AJAX kontrolok, de ezek
mennyisége, képessége elmarad más javascript keretrendszerek (Mootools, jQuery, Dojo)
képességétől, választékától5, amiket sok más fejlesztői környezetben vagy CMS rendszerekben
beváltan használnak. Ha csak arra gondolunk, hogy az ilyen ’ más ’ keretrendszereknek milyen
méretű felhasználói/programozói/tesztelői tábora van és mekkora how-to példamennyiséggel
rendelkeznek és ezzel mennyi próbálkozástól (négyszemközt erősebb kifejezést használnék)
kímélhetjük meg magunkat, már ez is nyomós indok lehet egy technológiai váltás mellett. „De
hát lehet jQuery-t használni ASP.NET alatt is!” Valóban, de ezek nem voltak gyerekkori
játszótársak, csak az ASP.NET 4.0 óta barátkoznak. Pedig ez fontossá válik akkor, ha tényleg
elkezdjük együtt használni őket, főleg, ha négyet: ASP.NET + AJAX + jQuery + jQuery-UI
használunk egyszerre. Mondjuk azért, mert megtetszik a jQuery-UI egységes ablakkezelési
módja és egységes kinézete, ami valahogy nem illeszkedik az ASP.NET theme/skin
rendszeréhez. Aztán jönnek még további érdekességek, mikor szembe találhatjuk magunkat
azzal is, hogy az ígéretesnek tűnő jQuery a szelektorjában valami egyszerűt vár pl.: ilyet:
$(’#textbox1’), viszont az ASP.NET HTML Id generátora ennél sokkal ravaszabb ID-ket generál
automatikusan , például.: ilyet: ’ct100$KozepPlaceHolder$SajatVezerlo2$Textbox2’. Aztán
továbbmegyünk és elgondolkozunk, hogy valóban jó megközelítés, hogy a document betöltése
után induló eseményre, a DocumentReady-re egy oldalon 4-5x is feliratkozunk a különböző
user controlok miatt? És így tovább.
5-10 éve egy HTML oldal betöltés végeredménye kb. olyan volt, hogy a letöltött fájltípusok
aránypárja így nézett ki: HTML : (JS+CSS) = 5:1, ma meg kb. így: HTML : (JS+CSS) = 1:10. Ez
pedig azt jelenti, hogy a kliens oldalon igen tetemes kód fut és a grafikus dizájnerek sem
tétlenkednek mostanában. Hogy ebből ne legyen káosz, ugyanazt az elvet kell alkalmazni, ami
a c# kódolásnál már evidens: tervezési minták, strukturált/rendezett átlátható kód, egységes
elnevezési konvenciók. Szinte kényszerű, hogy a HTML kód elkülönüljön a CSS stílusoktól és a
JS kódtól, amennyire csak lehet.
Az, hogy a un. ’Separation of Concepts’ elv szerint „az alkalmazásban levő funkcionális
egységek a lehető legkisebb mértékben függjenek egymástól” kicsit nehezen teljesíthető egy
hagyományos ASP.NET-es megközelítésben, ahol az például az SQL adatforrást az .aspx oldalba
5
http://en.wikipedia.org/wiki/Comparison_of_JavaScript_frameworks
2.5 Bevezetés - ASP.NET MVC platform előnyei 1-15
(a html sorok közé) szokták beékelni (sok-sok oktatóanyag példája alapján…). A Dependency
Injection használata és a Unit tesztelés MVC-ben lényegesen egyszerűbb.
Ide kívánkozik, hogy az ASP.NET Web Forms 4.0 verzió megjelenésével nagyon sokat fejlődött.
Belekerültek olyan szolgáltatások, amik az MVC-ben debütáltak előzőleg. Szóval már nincs olyan
nagy differencia, mint mondjuk a 4.0 előtti Web Forms és az MVC 3 között. A helyzet bizonyára
tovább javul a 4.5 után megjelenő verziókkal, és a különbségek is csökkennek majd. A következő
generációs Web Forms oldalsablonok is már sokkal szofisztikáltabban különítik el a HTML
markupot a háttérkódtól.
- Az MVC forráskódja a kezdetektől hozzáférhető a CodePlex-en. Nem függünk attól, hogy mikor
javítanak ki egy hibát a frameworkben, ha megtaláltuk, akár magunk is kijavíthatjuk. Sőt, akár
a fejlesztésnek is részesei lehetünk, több módon is. Ez a lehetőség jól fog jönni majd akkor, ha
saját kiegészítőket kezdünk írni. Az MVC-vel kapcsolatban soha nem volt szükség arra, hogy
majd egy ASP.NET Guru megmondja mit és hogyan kell megoldani. Ott a forráskód. Meg lehet
nézni, le lehet fordítani. Akár végig lehet lépkedni a debugger-el a teljes oldalfeldolgozáson.
(ASP.NET 4.0 óta a Web Forms is open-source )
- Az MVC az egy évtizede folyamatosan fejlődő, javuló ASP.NET kódjára épül. Tehát az alapok
igen jól teszteltek, hatékonyak és hibatűrők. A request, a response, a session objektumok, a
cache kezelés és a security szempontjából teljesen a tradicionális ASP.NET alapokra építkezik.
- Az MVC-ben bevált technikák annyira jól sikerültek, hogy visszahatottak az ASP.NET
alaprendszerre is és annak is integráns részei lettek. Ilyen például a routing és a model binder
megvalósítása.
- Könnyen integrálható bármely JS keretrendszerrel és az MVVM mintával.
- Minden további nélkül egy alkalmazásban lehet használni ASP.NET Web Forms és MVC
oldalakat. Tehát a meglévő projektek technológiai váltása nem vagy-vagy alapon zajlik, hanem
lehet apró lépésekben is átmigrálni.
- Mivel a kód és a megjelenítés rendkívül jól el van szeparálva, kevés szoros függőség van a
modulok között, ezért nagyon egyszerű automatikusan tesztelni.
- Nem kerül a generált HTML kódba semmi olyan, amit nem mi raktunk oda. Precízen kézben
tudjuk tartani az egész folyamatot, az elemek elnevezését, és a javascriptek eseménykezelését
is.
- Az egész MVC framework úgy épül fel, hogy adja magát, hogy azokat az irányelveket
alkalmazzuk, amelyek bármely többrétegű architektúrában melegen ajánlottak. Teljesen
természetes, hogy ORM mappert fogunk használni, szolgáltatásokat fogunk hívni az
alkalmazásunkból. Az oldalainkat hierarchikus template-kből állítjuk össze és a modelljeink
fogják tartalmazni a validációs szabályokat. De ezekre nincs megkötve a kezünk.
- Ha nem tetszik valamelyik része a frameworknek megvan a módja, hogy lecseréljük azokat
kedvünk szerint. Ha nem jó nekünk a View sablonunk nyelve, hát írhatunk egy másikat. Semmi
sem köt minket ahhoz, hogy az ASP.NET-ben megszokott <%%> közé írjunk vagy a Razor
szintaxis szerint a @ után írjunk kódokat. Valójában az MVC framework képességeit a
legalacsonyabb szinttől kezdve felülbírálhatjuk.
2.6 Bevezetés - Az ASP.NET és MVC framework 1-16
- Véleményem szerint, habár nincs róla statisztikai adatom, csak a saját tapasztalatom, de egy
PHP vagy egy Java web fejlesztő sokkal könnyebben megérti, mint az ASP.NET hagyományos
Web Forms változatát. Szerintem könnyebben tanulható.
Olyan szavak mindenképen ráillenek, hogy innovatív, rugalmas, gyors, bővíthető és korszerű.
Ha ránézünk erre a technológiai blokksémára, látható, hogy az MVC az ASP.NET alaprendszerre épül
rá, hasonlóan a zöld sávban levő többi technológiához.
Régebben az ASP.NET és a Web Forms gyakorlatilag egyet jelentett. A .NET 4.0 megjelenésével együtt
átalakult a technológiai platform. Az MVC 4.0 és a Web Forms 4.0 két fejlesztési alternatíva lett. Az
MVC mellett megjelentek további lehetőségek, amikkel ez a könyv nem foglalkozik. Ezek a fenti ábrán
is megtalálható Single Page Apps, WebAPI és SignalR platformok. Viszont pont e könyv írásának idején
jelent meg Reiter István jóvoltából a WebAPI6-ról egy könyv, amit mindenképpen ajánlok az olvasó
figyelmébe, miután túljutott e könyv olvasásán.
Elsőként szükség lesz egy megalapozott C# tudásra. A példakódok könyvben kizárólag ezen a nyelven
fognak szerepelni. Szerencsére rendelkezésre áll magyar nyelven több kiváló szakkönyv is.
A példák Visual Studio 2012-ben lesznek bemutatva, ezért szükségünk lesz egy Visual Studio példányra.
Ha nem áll rendelkezésre, talán megéri a könyv elolvasásának az idejére egy próbaváltozatot7 letölteni
és használatba venni. További lehetőség a Visual Web Developer Express 8 , ami ingyenesen
használható.
Az MVC 4 framework a VS 2012-vel együtt települ fel a gépre így ezzel nincs semmi dolog. A Visual
Studio ezt megelőző 2010-es változatához illeszkedő telepítőjét a http://www.asp.net/mvc/mvc4
oldalról érdemes letölteni. Itt található kétfajta telepítő. Az egyik a Web Platform Installer alatt
6
https://devportal.hu/Fajlok/Default.aspx?shareid=1&path=Konyvek%5cASP.NET+MVC+Web+API
7
http://www.microsoft.com/visualstudio/eng/downloads
8
http://www.asp.net/vwd
2.2 Bevezetés - A böngészőkről 1-17
Akik pedig mélyebben szeretnék tanulmányozni az MVC működést azoknak érdemes letölteni az MVC
4 és 5 forráskódját innen: http://aspnetwebstack.codeplex.com/ . Mint arról már szó volt az MVC túl
jól sikerült elsőre is ahhoz, hogy ne csak egy ASP.NET kiegészítőként élje az életét. Ezért a CodePlex
projekt neve sem az, hogy MVC, hanem az ASP.NET-re ráépülve: Asp.net Webstack.
A régebbi (v1, v2, v3) verziók még elérhetőek a http://aspnet.codeplex.com/releases oldalról. Ha nem
sajnáljuk rá az időt, letölthetjük a régebbi verziók forráskódját és összevethetjük az MVC 4-el és rögtön
szembe fog tűnni, hogy ez a legújabb változat már tényleg nem csak egy árva projekt mint régebben,
hanem az ASP.NET rendszer szerves része lett.
2.2. A böngészőkről
Mivel web fejlesztésről lesz szó, kelleni fog legalább két olyan böngésző, amit jól ismerünk. Tehát nem
csak annyira, hogy tudjuk, hova kell beírni az URL-t, hanem alaposan. Olyanra gondolok, amelyben
tudjuk, hogy
Kezdő MVC fejlesztőnek ajánlott az Internet Explorer (IE) legújabb verziója, legfőképpen azért, mert
fejlesztés során remekül együttműködik a Visual Studio-val, míg ez a többiről nem mondható el. A
következő részekben viszont a Google Chrome-ot, a FireFox-ot is fogom használni a példák során. Ezek,
beleértve az IE-t is, közös jellemzője, hogy mindnek van belső diagnosztikai modulja. (A FF-hoz a
FireBug9 nevű bővítményt érdemes letölteni.) Ha még nem ismerjük ezeket a weboldalak tartalmát
feltáró eszközöket, akkor egyszerűen nyomjuk meg az F12-őt a kedvenc böngészőnkben és nézzük meg
mit kapunk. A FF FireBug bővítménye:
A tabokon kategorizálva láthatjuk az aktuális oldal tartalmát, ami nagyságrendekkel jobb mintha a
nyers oldal forrását nézegetnénk. Ha nincs még alapos ismeretünk arról, hogy a böngészés közben mi
9
http://getfirebug.com/downloads
2.2 Bevezetés - A böngészőkről 1-18
is történik a háttérben, ezzel az eszközzel nagyon sok tapasztalatot tudunk szerezni. Nyissuk meg a
’Net’ fület és töltsük újra az oldalt!
Ezek a böngészőbe épített inspektorok annyira hasznosak, hogy komolyan ajánlom, hogy bármilyen
webes fejlesztés elkezdése előtt, előtanulmányként alaposan ismerjük meg egyet.
Van még egy eszköz, amit érdemes használni: ez a Fiddler10. Bár a legtöbb esetben az előbb említett
diagnosztikai modulokban elérhető hálózati eseményeket naplózó lista elégséges, ajánlott mégis a
Fiddler beszerzése (ingyenes). Ezzel a HTTP forgalmat egész részletesen meg tudjuk figyelni, sőt vissza
is tudjuk játszani, így akár böngésző nélkül is tudjuk tesztelni az alkalmazásunkat. Segítségünkre lehet
biztonsági rések felfedezésében. Például, ha következmények nélkül vissza tudjuk játszani az előző
HTTP post eseménysort, akkor az esetleg biztonsági hiba is lehet.
Egy fontos tanács webfejlesztőknek: "Ne bízz a böngészőben!". Nem olyan rég majdnem
fellélegezhettünk. Úgy tűnt, hogy azzal, hogy az Internet Explorer 6-os verzióját kivonják a forgalomból,
megszűnnek a böngészők közti kompatibilitási eltérések. Végre minden böngésző betartja a szabványt,
de nem. Erre soha se alapozzunk. Ahogy eddig is voltak eltérések, úgy most is vannak és nyilvánvalóan
ez után is lesznek. A HTML 5 és CSS3 összes képessége még nincs egységesen, kiforrottan
implementálva a böngészőkben. A javascript motorok is igen képlékenyen változnak ilyen-olyan
irányban. Érdemes elgondolkodni azon, hogy a Html 4.01-es verzióját is csak átmeneti szabvány
(transitional mód) szerint használják a mai napig is a legtöbb helyen. Sok-sok elavult HTML taggel. Az
olyan közeget, ahol a múlt sincs rendesen lezárva és velünk él, a jövő/jelen is bizonytalan, nem
hívhatjuk stabilnak. Ha úgy tekintünk a webes szabványokra, mint ajánlásokra és nem kőbevésett
szabályokra, akkor sok frusztrációtól kímélhetjük meg magunkat az olyan esetekben, amikor valami
nem úgy jelenik meg a megjelenítőn, ahogy kéne.
10
http://www.fiddler2.com
3.1 Első megközelítés - Az MVC architektúra 1-19
3. Első megközelítés
Ez a fejezet amolyan bemelegítés, ismerkedés, bemutató célzatúnak készült alapozó. A fejezet végén
már el lehet kezdeni kipróbálni az MVC lehetőségeit, sőt javasolt is egy pici alkalmazást felépíteni
önállóan. A rákövetkező fejezetek alaposan nagyító alá veszik a fő komponenseket, ezek
együttműködését, a lehetőségeket, elmerülve a technológiai részletekben.
Az MVC (azon túl, hogy nyilvánvalóan egy betűszó, ami a Model-View-Controller hármas kezdőbetűiből
áll) lényegében egy olyan tervezési minta, aminek alapja az, hogy a program alapvető szerepei jól
elkülöníthető egységekbe vannak szervezve. Van egy modellünk (M), ami lényegében az adatunk
leírója, van egy nézetünk (V), ami meghatározza, hogy a modellünk hogyan jelenjen meg és van egy
kontrollerünk (C), ami kezeli az előbbi kettőt és reagál a böngészőből érkező kérésekre. Tehát, ha ezt
a technológiát szeretnénk használni a tervezés/programozás során, akkor érdemes a
gondolkodásunkat is ehhez alakítani. Kis paradigmaváltást igényel egy Web Form fejlesztés után, ami
általában nyűgös, de azt mondhatom, hogy megéri az átállást. A programozás rövid történelme alatt
áttekinthetetlen, nehezen bővíthető és karbantarthatatlan forráskódok sokasága született.
Fejlesztések húzódnak a végtelenségig, rendkívül rossz hatékonyság és magas költségek mellett.
Alkalmazások sokasága készült el úgy, hogy az üzleti logika a megjelenítéssel foglalkozó osztályába van
beledrótozva, mondjuk az ASP.NET oldal háttér-kód fájljába, sok esetben úgy, hogy a közvetlen
adatbázis elérés is innen történik. Mindezek azt eredményezték, hogy karbantarthatatlan
kódmaszlagból épültek kritikus alkalmazások. Számos módszertan született ennek kivédésére, de ezek
közül az egyik legfontosabb, hogy az alkalmazást funkcionális részekre érdemes bontani és többrétegű
felépítést célszerű választani. Ebben ad nagyszerű alapot az MVC tervezési mintára épülő
technológia11. Minden lehetőség adott, hogy az alkalmazás felelőségi köreit jól elszeparáltan tudjuk
implementálni és szakítsunk a régi, mindent egy helyre zsúfoló megoldásainkkal. A tervezésben és
később a megvalósításban pedig nagy segítség, ha a tervünket jól elkülöníthető egységekre tudjuk
bontani, amely egységek külön-külön tovább tervezhetőek és/vagy magukban kivitelezhetőek,
lecserélhetők, sőt kipróbálhatóak. Manapság egy web alkalmazás elkészítése igen sok tervezési,
fejlesztési és technológiai területet érint. A teljeség igénye nélkül: szolgáltatás orientált és
kommunikációs architektúrák, adatbázisok a maguk szerteágazó ismeretigényével, böngésző és mobil
specialitások és egy sor további nyelv az alap forrásnyelv mellett (HTML, JS, CSS, T412).
11
Ez nem csak az ASP.NET MVC-re igaz, hanem a minta következménye. Idézet a Java világból: "A Velocity
kikényszeríti a Model-View-Controller (MVC) fejlesztési stílust, elszeparálva a Java kódot a HTML sablontól. Nem
úgy, mint a JSPs".
12
T*4 -> Text Template Transformation Toolkit. Fájlkiterjesztése: .tt
3.1 Első megközelítés - Az MVC architektúra 1-20
tudásigényűek és eltérő jellegűek ahhoz, hogy egy üzleti intelligencia vagy WCF szolgáltatások
programozásában jártas fejlesztőt, nem biztos hogy érdekelni fog (vagy nem lesz hatékony) és viszont.
Nos, ezek után nézzük meg az oly sok helyen fellelhető és valószínűleg ismerős hármas tagozódás
sémáját.
Controller Model
View
Ez az ábra nem tartalmaz köröket és nyilakat mint máshol szokott lenni, ugyanis szerintem ez nem
lényeges, sőt egyes rajzok megtévesztőek is. A fontos hogy lássuk, hogy van három jól elkülöníthető
funkcionalitású blokk (mackó sajt), ami külön-külön részegységeit alkotja a fő feladatnak, hogy
kiszolgáljunk egy dinamikus weboldalt.
Az MVC-nél a működési folyamatot, más néven az oldal életciklusát, a böngészőből érkező request
indítja el. Az MVC framework e kérést a kontrollerhez juttatja, annak is egy metódusához, amit
actionnek13 szoktunk nevezni. A metódus létrehozza a modell példányt, majd meghatározza, hogy a
modell alapján az MVC melyik View-t használja fel a böngészőnek küldendő válasz (a response)
létrehozásához. A következő ábra szemlélteti, hogy az általunk implementálható M-V-C hármasunk
valójában az MVC framework által szorosan felügyelt folyamat részállomásaiban kerülnek
felhasználásra.
MVC framework
Request Controller
View + Layout Response
http://localhost/ példányosítás.
kiválasztás. visszaküldése
Home Action kiválasztás.
Modell a Modell a
View
A mi M-V-C alkalmazásunk
request View
+Modell
alapján számára
Response
A kész html oldal
Az ábra csak egy elemi, alapszintű műveletsort szemléltet, a valóság ennél kicsit összetettebb, de erről
is lesz majd szó. Az ASP.NET MVC framework az MVC tervezési minta egy olyan részleges
megvalósítása, amiben az infrastruktúra lényeges részeit már leprogramozták helyettünk. Nem kell
13
Sajnálatos módon az „action” szó igencsak „túlterhelt”. Ez a neve a HTML formot feldolgozó URL attribútumnak
és a .Net generikus, paraméter nélküli delegate-jének. Ez utóbbi azonban szóba sem kerül később.
3.2 Első megközelítés - A Modell 1-21
bíbelődnünk azzal, hogy a request típusmentes adatait (URL, input mezők, stb.) egy osztálypéldányban
kapjuk meg vagy esetleg metódusparaméterekként. Nekünk csak meg kell írni a kontrollert a
metódusaival, de azok hívásáról az MVC rugalmasan paraméterezhető, aktiválórendszere
gondoskodik. Láthatjuk majd később, hogyha valamelyik előre megvalósított szolgáltatása nem
megfelelő, akkor elég egyszerűen lecserélhetjük. Ez annyira igaz ezzel a keretrendszerrel kapcsolatban,
hogy néha az az érzésem támad, mintha ezt is akarnák a fejlesztői.
Mikor jelenlegi webes alkalmazásokat egy skálán próbáljuk elképzelni, akkor a skála kezdetére tehetjük
azokat a megvalósításokat, amik egy menetben generálnak egy nagy HTML oldalt és ezt egy nagy HTTP
post alkalmával dolgozzák fel (jellemzően a lap alján ott a 'Ment' gomb). A másik vége a skálának, amit
mikrointerakciós oldalaknak szoktak nevezni, ahol minden egyes kis oldalrészlet teljesen saját életet él
és reagál a felhasználó műveleteire. Ezek a gombok, listák, csúszkák egymásra hatnak és a szerverrel
pici kis csomagokkal kommunikálnak. Ezeken a vezérlő és kijelző darabkákon kívül nem jellemző, hogy
szükség lenne egy olyan nagy Ment gombra. Az egész MVC keretrendszer és az általa egyszerűen, kevés
gyakorlattal megvalósítható alkalmazás, valahol középúton található. Természetesen meg lehet
valósítani mikrointerakciós oldalak kezelését is, de az ilyen esetekre a célszerűség jegyében már
megszületetett az ASP.NET WebAPI és számos JS keretrendszer.
3.2. A Modell
Kezdjük az M-el, talán azért is mert ez a legegyszerűbb. Végül is a modell nem más, mint összetartozó
adatok halmaza. Leggyakrabban egy szimpla osztály más esetben egy lista, megint más esetben egy
olyan osztály, aminek a tulajdonságai listák, vagy más osztályok. Lehet egy integer vagy akár egy egész
objektum gráf. A modell az adatspecifikációja annak a kapcsolatnak, ami az MVC keretrendszer, a View
és a Controller között végbemegy. Konkrétabban, ha azt szeretnénk, hogy a felhasználónak
megjelenjenek a saját profil adatai, akkor definiálunk egy nevet, email címet, születési dátumot
tartalmazó osztályt és ezt példányosítjuk a kontroller action metódusában, majd átadjuk a View-nak.
Előre szólok, mert később zavaró lehet, hogy a kisebb demóalkalmazásoknál gyakran előfordul, hogy
nincs is modell definiálva a kontroller actionje és a View számára. Lehetséges olyan helyzet is, hogy a
"nagybetűs" modell nem más, mint egy nagy semmi, egy null, mivel vannak más módok is, hogy
adatokat adjunk át a View-nak (pl. ViewBag).
3.2 Első megközelítés - A Modell 1-22
[Required]
[Display(Name = "Születési idő")]
[UIHint("Birthday")]
public DateTime BirthDate { get; set; }
}
2. példakód
Egy lehetséges eredményt mutat a következő ábra, miután megnyomtam a Save gombot:
Jogos kérdés, hogy van az, hogy az adatmegjelenítést a modellben szabályozzuk és nem a View-ban,
ahogy az tiszta lenne (pl. a fenti példában az Display attribútummal)? Érdemes azt is szem előtt tartani,
mielőtt a modellt és a View-t elsőre szoros párnak gondolnánk, hogy nem vagyunk korlátozva abban,
hogy egy modellt több View-hoz is felhasználjunk. Képzeljük el, hogy van két View a fenti Profile
modellhez. Az egyik megjeleníti a nevet és az email címet, míg a másik mind a három adatot. Előnyös,
ha a megjelenítést és a modellvalidációt egy közös helyen definiáljuk ilyen esetben. Ezért célszerű
létrehozni egy univerzálisabb adatmodellt ahelyett, hogy minden egyes View-ban újradefiniálnánk a
megjelenítési szabályokat.
3.3 Első megközelítés - A View 1-23
További érv is szól a jól definiált modell mellett. A felhasználói űrlapot (form) tartalmazó weboldalt
legalább két esetben is kezelni kell. Az első, amikor elkészíttetjük az MVC-vel az üres űrlapot (benne a
validációs előírásokkal, display nevekkel, esetleg előre kitöltött mezőkkel). A második, amikor
feldolgozzuk a felhasználótól érkező kitöltött űrlapot. Ebben is nagy segítségünkre lesz az MVC, ugyanis
a kitöltött űrlapot (aminek az adattartalmát előzőleg a modell alapján definiáltuk) szintén ugyanolyan
típusú kitöltött modellben kaphatjuk vissza, ha akarjuk. Ráadásul az adathelyesség ellenőrzését, és a
validációt is egyszerűen elvégzi helyettünk a rendszer. Egy ilyen metódus így szokott kinézni a bemeneti
paraméterként kapott feltöltött modellel:
Ezt az óriási előnyt, automatikusan a model binder nevű okos mechanizmus szolgáltatja. Összefoglalva,
a modell minimálisan nem más, mint egy üzenetcsomag és a csomagjellemzők újrafelhasználható
definíciója.
3.3. A View
A View egy sablon, vagy más szóval egy template. Template-eket akkor szoktak használni, amikor a
generálandó kimenet változatlan és változó tartalmú szakaszokból épül fel. Mint pl. egy körlevél, ahol
csak a címzett megszólítása és neve tér el, de a levél törzse egyforma (statikus) minden kiküldendő
levélben. Ilyenkor a címzett nevét valamilyen markerrel, tokennel, mezőnévvel helyettesítjük, ami a
levél generálásának pillanatában kicserélődik a konkrét névre.
A View-ban a statikus szakaszok HTML nyelvi elemekből épülnek fel. A dinamikus adatok, nos… a bőség
zavarában vagyunk. Ugyanis két sablon nyelvet is kapunk egyszerre az MVC framework-el a 3-as verzió
óta, és ezt a kételemű listát is bővíthetjük, ha nekünk nem megfelelőek.
- Az első az ASP.NET Web Forms hagyományos <% %> markerek közé zárt kódnyelve, ami egy régi
örökség. Az első MVC framework kiadások (1. és 2.) csak ezt értették. Ez főként azoknak lesz megfelelő,
akik járatosak az ASP.NET Web Forms szintaxisában. Ebben a példában egy egyszerűsített View sablon
látható ezzel a szintaxissal:
A fenti View sablon csak a Name property megjelenítését tartalmazza a Profile modellből. Az email és
a születési idő az áttekinthetőség kedvéért nincs benne.
Az ilyen tartalmat természetesen .aspx kiterjesztésű fájlba kell menteni. A sablonban szabványos HTML
oldalelemekbe vannak beágyazva a dinamikus szakaszok, <% %> jelekkel határolva. A dinamikus
szakaszokba C# kódot lehet írni (vagy VB.Net-et, ha valaki azt preferálja). Az első sorban levő Page
direktíva Inherits attribútuma azt határozza meg, hogy a View milyen típusú. Ebben az esetben egy
generikus ViewPage, aminek a generikus paramétere a modellünk típusa, amit a 2. példakódban
definiáltunk. Érdemes megfigyelni, hogy az MVC aspx fájljának a hagyományos értelembe vett code-
behind-ja igazából nem fontos számunkra. Ugyanis, amit az ASP.NET esetében az ilyen háttér kód .cs –
ba vagy .vb kiterjesztésű fájljába szoktunk írni - ami az oldal eseményeit, adatkötéseit kezeli le – az a
programlogika itt az MVC architektúrában a kontrollerbe kerül.
- A másik egy nagyban egyszerűsített template nyelv a Razor. Az elnevezés nagyon képies, összevetve
az ASP.NET borostás tartalmú fájljával, egy leborotvált tiszta kódot használhatunk a Razor szintaxissal
írt View fájlban, amiből hiányoznak a <% %> „szőrök”. Itt a kódblokkokat @ jel (malacfarok, ezt nem
viszi a borotva ) vezeti fel, a kód blokk végét nem kell külön jelölni. Ez persze furcsa elsőre, hisz az
(X)HTML és az Web Forms <% %>formátuma is a nyitó és záró tagek koncepcióját használja. Ennél a
nyelvnél nincsenek lezáró markerek. A sor végét "kitalálja". Az előbbi aspx template Razor nyelven,
fele annyi kódjelölővel, sárgával kiemelve:
@model MvcApplication1.Models.Profile
<!DOCTYPE html>
<html>
<head>
<title>ProfileRazor</title>
</head>
<body>
@using (Html.BeginForm()) {
@Html.ValidationSummary(true)
<div class="editor-label">
@Html.LabelFor(model => model.Name)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.Name)
@Html.ValidationMessageFor(model => model.Name)
</div>
}
</body>
</html>
4. példakód
A fájl kiterjesztése .cshtml vagy .vbhtml, ami a View-ba ágyazott kód nyelvére utal. Aki PHP
programozási tapasztalattal rendelkezik, annak nem lesz meglepő a formátum, hisz abban a világban
tucatjára állnak rendelkezésre ehhez hasonló template nyelvek (Smarty, Xtemplate, Blitz,…). Az
ASP.NET gyakorlóinak kicsit idegen lehet, legalábbis nekem az volt elsőre, de néhány próbálkozás után
rá kellett jönnöm, hogy jobban áttekinthető View-t lehet vele definiálni, ami a fenti példáknál
összetettebb View készítésénél válik igazán hasznossá. Elég csak az első sort megnézni, ami szintén a
modell típusát határozza meg:
3.4. A Kontroller
Következik a C összetevő, ami a Controller ősből származtatandó saját osztály, aminek az osztályneve
a 'Controller' szóval kell végződnie. Íme, rögtön egy példa:
Mint már említettem a kontroller tartalmazza a request kiszolgálásához szükséges kódunkat. A request
magában foglalja azt az URL-t, ami alapján a böngésző a válaszban a tartalmat várja. Például, ha az
URL így néz ki: http://localhost/Home/Contact, akkor az MVC framework, a (route) konfigurációnak
megfelelően a domain név utáni részt (/Home/Contact) megvizsgálva úgy fogja értelmezni, hogy
HomeController Contact() paraméter nélküli metódusát kell meghívnia. A Contact() action metódus
visszatérési értéke egy ActionResult-ból származó osztály, amit a Controller ősosztályban
megvalósított View() metódus tud előállítani. Névkonvenció alapján dől el, hogy melyik View template
lesz az, ami sablonként fog szolgálni a dinamikus HTML tartalom előállításához. Jelen esetben a View
az Action nevével egyező „Contact.cshtml” fájl lesz. Az elérési útja a projekt gyökeréből:
Views/Home/Contact.cshtml.
Ezt nagyon fontos jól megérteni. Itt nem egy fájl megcímzéséről van szó, mint az ASP.NET Web Forms-
ban a default.aspx fájl vagy PHP-ban az index.php esetén. Úgy is fel lehet fogni, hogy parancsokat,
utasításokat adunk az alkalmazásnak az URL-en keresztül. "A Home szekcióból kérem a Contact
adatokat prezentáló HTML tartalmat". Vagy továbblépve a példán, azt az utasítást, hogy "Kérem a User
szekcióból a Profile adatok közül a 15-ös azonosítóval rendelkezőt" így lehetne URL-ben
megfogalmazni: http://localhost/User/Profile/15
Az előbbi példakódban már látszik, hogy a Contact() metódusban létre van hozva egy ContactModell
példány, aminek a tulajdonságai beállításra kerülnek, és ez a modell paraméterként kerül átadásra a
View() metódus számára. A Profile() metódus pedig elégséges a Profile modell példányosításával arra,
hogy a 1. ábra szerinti weboldalt kapjuk. A kontroller az oldal generálás lelke. Olyan előfordulhat, hogy
nincs modell definiálva, ezen kívül olyan is, hogy View sem, de kontroller és action nélkül nem megy.
Ennyi alapelmélet és bevezető után szükséges, hogy a gyakorlati síkra ugorjunk át. Felteszem, hogy egy
Visual Studio-t sikerült beszerezni, a fejlesztési környezet is kész. Akkor, most hozzunk létre egy MVC
alkalmazást a File menu, New project… menüpontjával! A projekt sablonok közül válasszuk ki az
ASP.NET MVC 4 Web Application-t és adjunk egy nevet a projektnek. (FirstMVCApp). A lista felett van
egy .Net Framework 4 –en álló keretrendszer választó. Ezzel lehet beállítani, hogy a projektünk melyik
.Net verzió szerint épüljön fel.
3. ábra
4. ábra
Empty: Azért teljesen nem üres. Tartalmazza az assembly referenciákat, egy global.asax fájlt
és egy minimális elrendezési beállítást.
Basic: Egy kicsivel több, mert itt a konvencionális projekt struktúra is meg fog jelenni.
3.5 Első megközelítés - Próbáljuk ki! 1-27
Internet és Intranet Application: Indulásnak jó lesz, mert egy működő MVC alkalmazást
kapunk három menüponttal. A kettő között az a különbség, hogy az Intranet, a felhasználók
Windows hitelesítésére van beállítva.
Mobile Application: Mint a neve is mutatja egy mobil megjelenésre optimalizált projektet
kapunk. Ebben a jQuery Mobile javascript kliens oldali keretrendszer lesz az aktív szereplő.
Web API: Egy egyszerű REST képes HTTP web szolgáltatást épít fel, ami leginkább a kliens oldali
javascriptek adatigényét tudja kielégíteni.
A próbához az „Internet Application” most megfelelő lesz. Be lehetne állítani, hogy a View Engine (így
hívják ami értelmezi a View tartalmát), ne Razor hanem az old-school ASPX legyen. Ezt „Razor”-on
érdemes hagyni, ha most kezdünk ismerkedni ezzel a technológiával. Ha az aspx szimpatikusabb, egy
próbát megér hogy miként néz ki egy ilyen MVC alkalmazás aspx template-ben megfogalmazva.
Az Internet Application template alapján a VS (Visual Studio) létre fog hozni egy mini web alkalmazást,
ami ráadásul még el is indul és megjeleníthető tartalma is van. További problémák elkerülése végett
és hogy lássuk működik-e az MVC környezetünk, indítsuk el az alkalmazást (pl. F5-el). Ezt egyébként
érdemes megtenni minden friss MVC telepítés után.
Views. Ez egy kitüntetett mappa, de nem úgy mint a Controllers vagy a Models, (ami csak egy ajánlás,
hogy oda tegyük a modelleket és a kontrollereket), ennek a belső felépítése is számít. A View mappa
alá olyan mappák vannak sorolva, amelyek nevei korrelálnak a kontrollerek osztályneveivel. Ezért azt
mondhatjuk, hogy a Controllers/HomeController.cs-ben deklarált HomeController osztály action
metódusai számára a View template-k a Views/Home mappában keresendők (elsődlegesen). A Home
mappa nevét a kontroller osztály neve végéről a Controller szót levágva kapjuk. Ebben a mappában
pedig olyan .cshtml kiterjesztésű fájlok találhatóak, amelyek nevei megegyeznek a kontrollerben levő
action metódusok neveivel. Emiatt volt lehetséges az, hogy az 5. példakód Profile() metódus végén a
View() metódusnak nem kell paramétert megadnunk, mert az előbbi névkonvenció alapján feltételezi,
hogy létrehoztunk egy Profile.cshtml fájlt a megfelelő mappában. Van itt még egy speciális Shared
mappa, ami a kontrollerfüggetlen, közös View-k számára van fenntartva, és a _ViewStart.cshtml fájl,
amiről később még részletesen lesz szó.
3.7 Első megközelítés - Új Model, View, Controller hozzáadása 1-29
Ha ezzel megvagyunk, mozgassuk az egeret a Controllers mappára és kérjünk egy helyi menüt. Bökjünk
a menü Add->Controller… elemére .
A dialógus ablakban töltsük ki a kontroller nevét, ügyelve arra, hogy a választott név után szerepeljen
a Controller utótag. A 'Template' legördülő listát is a képen látható módon állítsuk be.
8. ábra
3.7 Első megközelítés - Új Model, View, Controller hozzáadása 1-30
Miután alul az Add gombbal létre hoztuk a kontroller osztályunkat, nyissuk meg, mert még van vele
tennivaló. Amit kaptunk az egy olyan kontroller, ami egyelőre független a modellünktől, de félkész
action metódusok vannak benne a leggyakoribb műveletekre.
Index – Egy indító lap. A template által gyártott további actionök neve és működése miatt egy elemlista
szokott lenni. A listában szereplő sorokat valamilyen kiegészítő parancs oszloppal szokták ellátni,
amivel további műveleteket végezhetünk a sor mögött levő entitással, ami a alapján a sor meg lett
jelenítve.
Details – A listában nem szokás minden propertynek oszlopot készíteni, mert nem lesz áttekinthető.
Ezért, ha az előző actionre egy elemlista készítő funkciót képzelünk, akkor ez a Details action a lista egy
elemének a részletes nézete. Erre az áttekintő nézetre készült ez az action.
Create – Egy új elemet készíthetünk el, ami majd a listába kerül. Általában ez az action kezdeti értéket
szokott adni az új elemnek, amit aztán a felhasználó megváltoztathat és menthet. Azonban ezt az
actiont ne úgy képzeljük el, mint ami menti is az új elemet, csak előkészít.
Edit(int id) – Egy meglévő listaelem szerkesztési oldalát állítja össze, de nem menti el.
Edit(int id, FormCollection collection) – Ez az előző párja, ami menti a felhasználó által kitöltött
űrlapot. A Create és az Edit actionök is HTML formokkal (űrlapokkal) dolgoznak, szemben a Details-el,
ami pedig nem.
Delete(int id) – Ez lehetne az az action, ami képes a végleges törlés előtt még egy megerősítést kérni
a felhasználótól, mielőtt ténylegesen törölné az elemet a listából és az adatbázisból.
Delete(int id, FormCollection collection) – Nyilvánvalóan az előző párja, ami "elvégzi a piszkos
munkát".
De ezek nincsenek kőbe vésve, akármilyen névvel és funkciókkal hozhatunk létre action metódusokat.
Ezért volt az a sok feltételes mód, hogy ne úgy lássuk, mint egy előírást. Az egyetlen előírás, hogy a
kontroller neve Controller-re végződjön.
Folytassuk azzal, hogy elkészítettjük a View-kat is. Nyomjunk egy jobbgombot az Index action return
View() metódusán, és a menüsor Add View… pontjával létre is hozhatunk egy View-t.
9. ábra
Ez a modelligénye az Index View-nak, de mondhatjuk azt is, hogy ez a View típusa. Adjunk át neki egy
típusos felsorolást a kontroller Index actionben, úgy hogy a felsorolást a View() metódus paraméterébe
tesszük.
Még egy apróság maradt hátra, hogy a főmenübe egy menüpontot tegyünk az új kontrollerünk Index
actionjéhez. Nyissuk meg a Views/Shared mappában a _Layout.cshtml fájlt. Illesszük be a főmenük
menüpontjai után a First kontroller Index actionjére mutató linket, amit egy Html helper
metódushívással tudjuk megtenni.
3.7 Első megközelítés - Új Model, View, Controller hozzáadása 1-32
A vastagon szedett sor lenne az, a többi három sor már ott volt:
<nav>
<ul id="menu">
<li>@Html.ActionLink("Home", "Index", "Home")</li>
<li>@Html.ActionLink("About", "About", "Home")</li>
<li>@Html.ActionLink("Contact", "Contact", "Home")</li>
<li>@Html.ActionLink("Első próba", "Index", "First")</li>
</ul>
</nav>
Az ActionLink első paramétere lesz a link felirata, a második az Action neve, a harmadik a kontroller
neve (FirstController).
Most indulhat az első menet. A projekt fordítása utáni futtatáskor a böngészőben megjelenik az új
főmenü. Az új 'Első próba' menüpontra kattintva az Index által szolgáltatott listanézet ez lesz:
A sorok mellett ott vannak az Edit, Details és Delete linkek, amik ténylegesen a First kontroller azonos
nevű actionjeire mutatnak. Ha megnyomjuk valamelyiket csak egy sárga hibaüzenetet kapunk
(YSOD14), hiszen az actionök ugyan kész vannak, de a hozzájuk kapcsolódó View-k még nem.
A további actionök végén levő View() metódushívásokra elkészíthetjük a hozzájuk tartozó View-kat, az
Add View… menüponttal. Az egyetlen eltérés, hogy a „Scaffold template” legördülő listából hozzá kell
passzoltatni a megfelelő View generátor templatet az action funkciójához.
Megint csak annyi van hátra, hogy modelleket is kapjanak a View-k az action
metódusokból. Ezek az új View-k nem listát várnak, hanem konkrét
modellpéldányt, ezért egyszerűen minden (int Id) paraméterű action
metódusba beleraktam egy modellpéldányosítást:
14
Yellow Screen Of Death
3.7 Első megközelítés - Új Model, View, Controller hozzáadása 1-33
Ennek így nem sok értelme van, de adatbázis háttér nélkül nem sokat tudunk most csinálni. A lényeg,
hogy működnek az actionök, ha elindítjuk az alkalmazást. Használhatóak az Index oldal lista sorainak
végén az Edit, Details, Delete funkciók is.
<div>
@Html.ActionLink("Back to List", "Index")
</div>
Átírva:
<div>
@Html.ActionLink("Vissza a listához", "Index")
</div>
<td>
@Html.DisplayFor(modelItem => item.Address)
</td>
<td>
@Html.ActionLink("Szerkesztés", "Edit", new { id = item.Id }) |
@Html.ActionLink("Részletek", "Details", new { id = item.Id }) |
@Html.ActionLink("Törlés", "Delete", new { id = item.Id })
</td>
</tr>
}
Szeretném felhívni a figyelmet a minden sor végén ott levő paraméterekre. Ez egy anonymous
objektum, ami a link elkészítésénél azt a szerepet kapja, hogy az objektum propertyjeiből URL
paraméterek lesznek. Például a "Szerkesztés"/"Edit" link generálásának eredményeképpen létrejövő
link markupja így néz ki: <a href="/First/Edit/1">Szerkesztés</a>
Nem csak a kontroller és action neve kerül bele az URL-be, hanem a végére az "1" is, mert ez felel meg
az id propertynek (hogy miért, azt majd hamarosan meglátjuk). Az ehhez az URL-hez tartozó action
paraméterlistájában szintén ott szerepel az id:
Emiatt az Id paraméterében megjelenik majd az 1-es érték, ha a 'Szerkesztés' linkre kattintunk. Illetve
a 2-es érték, ha az alatta levő sor 'Szerkesztés' linkjére kattintunk, mert annál az id értéke 2 lesz. Ahhoz,
hogy a szerkesztés oldalon végzett módosításokat át tudjuk venni típusosan, csak annyit kell tenni,
hogy a másik (HttpPost attribútumos) Edit action paraméterét kicsit átírjuk ilyenről:
[HttpPost]
public ActionResult Edit(int id, FormCollection collection)
Ilyenre:
[HttpPost]
public ActionResult Edit(int id, Models.FirstModel model)
Mostantól, ha megváltoztatom például a 'Teljes név' beviteli mező tartalmát, és mentem az oldalt,
akkor az Edit actionbe megérkezik a módosított adatokat tartalmazó feltöltött FirstModel példány.
Ezt azért tudjuk így megtenni, mert a View létrehozásakor (a View dialógusablakban) beállítottuk a
modell típusát (FirstModel). Mivel így állítottuk be, a létrehozott View is típusosan lett legenerálva.
Idemásoltam az Edit.cshtml fájl idevonatkozó részletét.
@using (Html.BeginForm())
{
<fieldset>
<legend>FirstModel</legend>
<div class="editor-label">
@Html.LabelFor(model => model.FullName)
</div>
<div class="editor-field">
3.7 Első megközelítés - Új Model, View, Controller hozzáadása 1-35
<div class="editor-label">
@Html.LabelFor(model => model.Address)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.Address)
@Html.ValidationMessageFor(model => model.Address)
</div>
<p>
<input type="submit" value="Ment" />
</p>
</fieldset>
}
A vastagon kiemelt sorok hozzák létre a form input mezőit. Ezek neve meg fog egyezni a property
nevével: FirstModel.Address <input name="address"… >. Amikor a form a submit gomb
megnyomására elküldésre kerül, az MVC az input nevek alapján fel tudja tölteni az Edit(int id,
FirstModel model) action paramétereit. Természetesen csak azokat a propertyket tudja beállítani a
FirstModel típusú 'model' paraméterben, amihez rendelkezésre áll <input> is névazonosság szerint.
Most már tudjuk, hogyan kell elkezdeni az építkezést MVC-ben, nézzünk meg egy életszerűbb
példasort, amiben a modellünkben levő adatokat el is tárolhatjuk. Az MVC projektünkben a referenciák
között megtaláljuk az Entity Framework (EF) 4.4-es verzióját. Ez egy ORM mapper, ami az adatbázis
tábla (és egyéb) sémákat összerendeli a normál .Net osztályokkal. Ezzel objektumorientáltan,
típusosan tudjuk a táblaadatokat kezelni. Az un. code first megközelítés lehetővé teszi, hogy ne kelljen
adatbázissal, SQL-el, XML modellekkel, tervezői felülettel, séma mappeléssel foglalkozni. Egyszerűen
csak létrehozzuk az osztályunkat és minden egyebet rábízunk az EF-re. Ez majd létrehozza az
adatbázist, ha még nincs, és a táblákat is az előre definiált osztályaink alapján. Kis MVC alkalmazásoknál
teljesen járható megoldás, hogy az EF 'code first' osztályok legyenek az MVC modellek is egyben. Ezzel
jó sok munkától meg tudjuk magunkat kímélni. A következő példákban is így fogunk eljárni.
A cél az lesz, hogy egy szimpla névjegykártya regisztert készítsünk el. Először szükségünk van a jól
definiált modellekre (code first!). A modellpropertyket el kell látni minden olyan attribútummal, ami
jelezni tudja, hogy milyen szabályok szerint fogjuk használni azokat. Ez két irányba is jelez: MVC felé,
hogy milyen validációs szabályokat alkalmazzon a propertyhez kapcsolódó beviteli mezőkre. Illetve az
EF felé is, hogy milyen mezőtípusokat és mezőhosszakat használjon az adatbázistáblák létrehozásakor.
Nézzük meg ezt a FullName modellproperty definíciót példaként:
A neve alapján létre fog jönni egy FullName táblamező. A StringLength(100) miatt az MVC nem fogja
engedni, hogy 100 karakternél hosszabb szöveget adjunk meg. Ami jó is lesz, mert az EF a FullName
táblamezőt nvarchar(100) típusúra fogja beállítani ez alapján. Így többet nem is fogunk tudni tárolni
benne. A Required az MVC-nek azt üzeni, hogy követelje meg a felhasználótól a FullName beviteli mező
kitöltését a böngészőben. Az EF ezt az attribútumot úgy fogja értelmezni, hogy a FullName táblamezőt
'NOT NULL' megkötéssel kell létrehoznia. Ezek után már érthetőek lesznek a modellek:
[Display(Name = "Megszólítás")]
[StringLength(10)]
public string Title { get; set; }
[Display(Name = "Cégnév")]
[StringLength(200)]
public string Company { get; set; }
3.8 Első megközelítés - Próbáljuk ki menthető adatokkal! 1-37
[Display(Name = "Beosztás")]
[StringLength(150)]
public string Position { get; set; }
//Navigation property
public virtual ICollection<PhoneNumber> PhoneNumbers { get; set; }
}
[Required]
[StringLength(34)]
[Display(Name = "Telefonszám")]
public string Number { get; set; }
//Backreference Id
[Display(Name = "Névjegykártya")]
public int CardRegisterId { get; set; }
//Backreference
public CardRegister CardRegister { get; set; }
}
A modelleket tegyük a Models mappába, de az MVC szempontjából nincs jelentősége, hogy hova
rakjuk. A CardRegister modellnek szüksége van egy alapértelmezett konstruktorra, hogy a
PhoneNumbers-nek adjon egy konkrét listát. Az 'Id' mint bevált, egyedi azonosító név lesz a Primary
Key a táblában. Ezt nem szabályozza attribútum, egyszerűen névkonvenció alapon, a neve miatt lesz
elsődleges kulcs. Mindjárt odajutunk, hogy létrejön az adatbázis, de addig is íme a fenti modellek
alapján automatikusan elkészülő táblák meződefiníciói:
A nagy kontrollervarázsló
A sikeres fordítás után jön a modell alapú alkalmazásgenerálás15: Hozzunk létre egy új kontrollert a
helyi menüvel, a Controllers mappán:
<ul id="menu">
<li>@Html.ActionLink("Home", "Index", "Home")</li>
<li>@Html.ActionLink("About", "About", "Home")</li>
<li>@Html.ActionLink("Contact", "Contact", "Home")</li>
<li>@Html.ActionLink("Első próba", "Index", "First")</li>
<li>@Html.ActionLink("Névjegykártyák", "Index", "CardRegister")</li>
</ul>
Navigáljunk most az Index oldalra. Az első megnyitása néhány másodperces várakozással jár, mert
most jön létre a háttéradatbázis. Az adatbázis jelenleg üres, ezért csak annyit tudunk tenni, hogy
felveszünk új adatsorokat a 'Create New' linkkel.
15
Enyhe túlzással, de majdnem.
3.8 Első megközelítés - Próbáljuk ki menthető adatokkal! 1-39
A megjelenő oldalon a beviteli mezők érvényre juttatják a hozzájuk kapcsolt validációs szabályokat.
A Title (Megszólítás) mezőbe nem lehet 10 karakternél többet írni, mert a StringLength(10) attribútum
ezt határozta meg.
.display-label {
display: inline-block;
width: 15%;
background-color: #ddd;
margin-bottom: 4px;
padding-left: 5px;
}
.display-field {
display: inline-block;
width: 80%;
}
.phones {
border: 1px dotted #ddd;
padding: 10px;
}
Létrejönnek megint a
View fájlok is.
<div class="editor-label">
@Html.LabelFor(model => model.CardRegisterId, "CardRegister")
</div>
Az a felesleges szöveg felülbírálta a CardRegisterId propertyn levő Display attribútum hatását.
A Master-Details nézet
Valahogy még mindig nem komfortos. Azt kéne elérni, hogy a telefonszámokat is láthassuk a
névjegykártya részletes nézetében. Sőt rendelhessünk hozzá új telefonszámokat a Details nézetben,
és ne kelljen attól teljesen külön kezelni a PhoneNumber valamelyik oldalán. Nézzük sorban.
@model IEnumerable<FirstMVCApp.Models.PhoneNumber>
<h2>Index</h2>
<p>
@Html.ActionLink("Create New", "Create", new {cardid = })
</p>
<table>
<tr>
<th>
3.8 Első megközelítés - Próbáljuk ki menthető adatokkal! 1-41
Az ActionLink-ek módosítására azért volt szükség, mert ha nem nevezzük meg a kontrollert
(PhoneNumber), akkor a CardRegister lenne a végrehajtó kontroller. Az alapértelmezett működés az,
hogyha nem adjuk meg külön a kontroller nevét, akkor az adott View-t kezelő kontroller actionjeit
jelentik a megadott action nevek (minden sorban a második 'Edit', 'Details', 'Delete').
</fieldset>
<div class="phones">
@Html.ActionLink("Új telefonszám", "Create", "PhoneNumber", new { cardid = Model.Id }, null) <br/>
@Html.Partial("PhoneNumberPartial", Model.PhoneNumbers)
</div>
Várjon nullázható integert paraméterként, ami név szerint megegyezik az ActionLink végén levő
anonymous objektum tulajdonságnevével (narancssárga cardid). A SelectList objektum fogja
szolgáltatni a névjegykártyák legördülő listájának az elemeit. Az utolsó paraméterével lehet beállítani
az alapértelmezetten kiválasztott elemét. Ez most pont az a névjegykártya Id lesz, amelyik
3.8 Első megközelítés - Próbáljuk ki menthető adatokkal! 1-42
névjegykártyának a Details nézetéből idehivatkoztunk. A View-nak átadunk egy nagyjából üres modellt
csak a CardRegisterId-t töltjük fel.
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(PhoneNumber phonenumber)
{
if (ModelState.IsValid)
{
db.PhoneNumbers.Add(phonenumber);
db.SaveChanges();
return RedirectToAction("Details", "CardRegister", new { id = phonenumber.CardRegisterId });
return RedirectToAction("Index");
}
Ebben az actionben csak annyit érdemes változtatni, hogy ne a telefonszámok listájához ugorjon át, ha
jól töltöttük ki a formot, hanem a telefonszámhoz tartozó névjegykártya nézet oldalára. Vissza,
ahonnan elindultunk az 'Új telefonszám' linkkel. Ezt oldja meg a vastagon kiemelt RedirectAction a
paramétereivel.
A 'RedirectToAction' sort érdemes kicserélni az Edit és a Delete actionökben is. Hogy még teljesebb
legyen a felhasználói élmény.
Nézzük meg a Views/PhoneNumber/ alatti View fájlok végét. Mindegyikben ott lesz egy navigációs link,
ami visszamutat a telefonszámokat listázó PhoneNumber/Index oldalra ('Back to List'). Ha ezeket
kicseréljük az alábbi példa alapján, akkor az új link szintén a telefonszámhoz tartozó névjegykártya
oldalra fog visszavezetni.
Az ’Assembly name’ határozza meg, hogy a lefordított kódunk milyen nevű .dll kiterjesztésű fájlba fog
kerülni. A ’Target framework’ a futtatáshoz szükséges .Net keretrendszer verzióját határozza meg. A
lefordított kód legalább ilyen verziószámú .Net környezetben fog tudni futni. Ha ezt megváltoztatjuk a
Visual Studio a referált .Net dll-eket is megpróbálja aktualizálni az új beállításhoz. Ez néha sikerül, néha
nem, emiatt az új projekt létrehozásakor célszerű meggondoltan beállítani a 3. ábra felső részén levő
.Net verziót. Az ’Output type’ ’Class Library’, mert az MVC alkalmazás futásához ez kell.
Ez a ’bin\’ azt jelenti, hogy a projektünk gyökerében létre fog jönni egy bin almappa és a lefordított
kódok .dll fájljai ebbe fognak kerülni. Egy új VS projekt esetén ide másolódnak az MVC futásához
szükséges további dll-ek is.
Az ’Apply server settings to all users’ egy érdekes beállítási lehetőség, ha többen dolgozunk egy
projekten. Ha kivesszük a pipát, akkor az előzőleg tárgyalt beállítások a projekt mappa
[Projektnév].csproj.user fájlban fognak tárolódni és a projekt fájl közösen használható lesz, de a web
server beállítások, különösképpen a port szám, viszont felhasználónként egyedi lesz. Ez a
leghasznosabb, abban a ritka helyzetben, ha egy fejlesztői gépen egyszerre (pl. terminál szerverrel)
többen fejlesztenek. Így nem lesz portütközés, mert több fejlesztői webszerver tud futni más és más
porton.
16
Internet Information Services. A Windows operációs rendszereken futó webkiszolgáló.
3.10 Első megközelítés - A MVC komponenseinek működési ciklusa 1-45
Az ábra jobb oldalán látható egy második kontroller (Common), ami egy külön belső MVC
oldalgenerálási eseménysort szolgál ki, amit nem a böngésző, hanem a lenti View-ban levő kód indít
el, mert mint látni fogjuk ilyenre is van lehetőség.
Ez a fejezet egy madártávlati képnek készült, első rárepülésként a témára. Remélem sikerült a legtöbb
alapfogalmat megemlíteni. Ha valami még nem tiszta, vélhetően hamarosan minden világossá fog
válni. A következő három nagy fejezetben az MVC három fő szegmensét nézzük meg alaposabban.
4.1 Modell - Modell és tartalom 1-46
4. Modell
Mikor ez a három fő fejezet (Modell-Kontroller-View) már 1/3 részben kész volt, elkezdtem
gondolkozni, hogy vajon jól vannak-e sorrendbe rakva és bizony gondban voltam. Ugyanis ezek a témák
nagyon erősen összefüggenek. Nehéz úgy részletesen beszélni az action metódusokról és
paramétereikről, hogy előtte ne tárgyaljuk a route beállításokat, amit viszont nehéz elsőre részletesen
bemutatni, ha az action metódusokról előtte nem beszéltem. Hasonlóan van ez a modell és az action
viszonylatában is. Emiatt ezeknek a fejezeteknek az elolvasásához azt tudom tanácsolni, hogyha elsőre
nem világos valami, akkor csak haladjunk tovább, és ha a három fejezet végén még mindig sok a kérdés,
akkor még egyszer érdemes átolvasni. [Ha ezek után sem, akkor lehet, hogy le kéne fordítanom a
könyvet magyarra…]
Az 3.2 fejezetben néhány dolgot megmutattam a modell szerepéről, de azóta kicsit nagyobb rálátással
bírunk az egészre. Mint említettem a modell elődleges célja nem más, minthogy egy típusos
adathordozó (osztály) legyen. Emiatt, a modellre nem annyira jellemző néhány objektum orientált
tervezési ajánlás. Olyanokra gondolok, hogy az objektumnak csak egy oka legyen a megváltozásra, csak
egy felelőségi köre legyen, stb. Elsődlegesen ez egy típusos tároló, minden más szempont csak ez után
következik.
Végül is rajtunk áll, hogy mit teszünk a modellbe. Ami fontos, hogy belekerüljenek a HTML kódban
megjelenő mezők, táblázatok adatforrásai. Mindenesetre van néhány szempont, amit érdemes
átnézni, hogy mennyire legyen bőbeszédű a modellünk, milyen esetben hogyan határozzuk meg a
modell tartalmát. A kérdés most elsősorban az, hogy a modell mennyire van jelen az alkalmazás többi
rétegében és mennyire kötődik az adatbázis vagy a szolgáltatás (pl. WCF) sémájához.
Így vagy úgy a property nevekből HTML input mező nevek és HTML id attribútumok képződnek.
<input name="propertynev" id="propertynev"/> Ezért a név meghatározásánál nem csak
azt kell figyelembe venni, hogy megfelel-e a C#/CIL elnevezési szabályának, hanem a HTML
attribútumokra is tekintettel kell lenni. A magyar ékezetes neveket mindenesetre kerüljük.
Szintén kerüljük el a HTML szabványos attribútumneveit (checked, disabled, form, stb.)
A C# kisbetű-nagybetű érzékenyen megkülönbözteti a neveket. A HTML form értelmezésénél
a böngészőket ez nem zavarja. Egy formon levő azonos nevű input mezők gondot okozhatnak
önmagukban is, de a bejövő request alapján az MVC által példányosított modell feltöltése nem
case-sensitive. Az <input name="nev" és a <input name="NEV" azonos modellpropertyhez
tartozik.
A property nevek ne legyenek benne a route szakaszdefiníciók nevei közt használtakban:
{controller}/{action}/{id}. Ez egyszerűbben szólva annyit jelent, hogy az "action" és a
"controller" neveket tekintsük foglalt névnek és ne használjuk.
Készítettem egy nagyon rossz modellt, amolyan állatorvosi lovat, amivel részben szemléltetni tudom,
milyen neveket nem kéne használni.
A "Name" property három változata alapján létrejövő HTML blokk ugyan követi a nevek írásmódját, de
a form beküldésekor már csak az első "Name" változat fog megérkezni az action metódushoz.
Ezzel az actionnel is probléma lesz. Mind a három string paraméterben a "Name" értéke fog
megjelenni, jelen esetben a "Tanuló 1".
A problémák abból adódnak, hogy a WrongModel property neveiből dictionary kulcsok lesznek, ahol a
kis-nagybetű eltérés nincs figyelembe véve. Természetesen senki sem szokott közel azonos, publikus
nevekkel operálni egy osztályon belül, de gondoljunk az öröklésre is. A modell ősosztályában levő
tulajdonságokkal se legyen ütköző elnevezés.
Mivel a modellünk egy osztály, semmi sem gátol minket abban, hogy a modellbe az adatokhoz szorosan
kötődő kódokat írjunk. Most csak két megvalósítási irányelvet mutatnék be nagyvonalakban. A valóság
nem ennyire szélsőséges és merev, mintha csak két lehetőségünk lenne, hanem inkább ezek keveréke
jellemző. Mindenesete egy jól szervezett kódban könnyebb eligazodni.
Itt arról van szó, hogy a View-ban megjelenő kódot (hamarosan lesz róla szó) minimalizáljuk és amit
lehet áthelyezzük a modellbe. A legtöbb View-ban lesznek olyan részek, amikor egy HTML tulajdonság,
CSS stílus vagy CSS osztály a modellben definiált egy vagy több property tartalmától függ. Sőt az is
tipikus eset, amikor azt akarjuk, hogy egyes szakaszok a View-ban csak bizonyos esetekben legyenek
egyáltalán figyelembe véve az oldalgeneráláskor. Erre egy példa, hogy nem érdemes megjeleníteni a
felhasználó nevét, amíg nincs bejelentkezve (mivel nincs is elérhető felhasználói profil adat), tehát az
ezt előállító View szakasz kimaradhat. Ilyen esetben a megjelenést szabályzó értékeket sokszor
(szerintem helytelenül) a View elején egy @{..} blokkban számolják ki, és hoznak létre helyi változókat.
Pedig ez nagyon ellentmond a feladatok elszeparálásának elvének. Ráadásul az ilyen kódok csak
futásidőben kerülnek lefordításra, tehát ha hibás, az túl későn derül ki. Ilyenkor lehet bevetni ezt a
koncepciót, és akkor az adatok és az adatokból számított további értékek is a modellben lesznek.
4.2 Modell - Modell és kód 1-49
Ez a kívánalom, de a másik oldalnak is van egy előnye (hogy a kód egy részét a View-ba rakjuk), ez pedig
az, hogy a View-ban levő kódot futásidőben, projekt build nélkül is tudjuk javítani, és ez nagyon
pragmatikussá teszi a dolgot. Majd a View részletes ismertetésénél megemlítem még egyszer, de a
lényege, hogy ha a View fájlban módosítunk valamit, akkor azt (még mindig futásidőben vagyunk)
újrafordítja az MVC és úgy használja fel. Ezzel a View-t gyorsan lehet javítgatni, okosítani, tesztelgetni.
Viszont ha megvagyunk ezzel az ad-hoc fejlesztési módozattal, utána érdemes a jól működő
kódrészleteket a modellbe átrakni.
Nézzük meg egy egyszerű példán keresztül, mit is jelent ez a modell felépítési mód. Az alábbi
kódrészletben a modell propertyjeinek adatait további, csak olvasható tulajdonságok értelmezik. Ezek
egyértelműsítve adnak eredményeket a modell belső állapotáról. Például, ha a szállítás dátuma
elérhető, arról egy boolean érték ad tájékoztatást. Ez így nagyon triviálisnak tűnik, mert hát a View
kódjába is bele lehetne írni, hogy NullázhatóDátum.HasValue. Ennek a megközelítésnek akkor lesz
haszna, ha a „szállítás dátuma elérhető” mint állapot, az üzleti igény változása miatt nem csak a dátum
értékétől/meglététől fog függeni, hanem mondjuk egy fő diszpécser jóváhagyásától is. Ilyenkor fogunk
örülni, hogy nem a View-ba írtuk a kiértékelést. Illetve nem 25 különböző View-ban ismételtük meg a
kiértékelő kódot.
//Számított értékek
public bool DeliveryDateAvailable
{
get { return TransportDate.HasValue; }
}
Ha az ügyfél VIP
{
A nevének a kiiratásánál alkalmazd a következő CSS osztályt: ”vipcustomer”
}
Egyébként
{
A nevének a kiiratásánál alkalmazd a következő CSS osztályt: ”normalcustomer”
}
Ezek a számított értékek az esetek egy jó részében boolean típusúak, tehát csak egy döntés
eredményét hordozzák. Igazán hasznos tud lenni ez a megközelítés, ha arra gondolunk, hogy a
modellünket több View-val kapcsolatban is használni szeretnénk. Ekkor a megjelenítéssel összefüggő
feltételek az egységes modellben értékelődnek ki, emiatt a View-k megjelenési szabályrendszere is egy
helyen lesz karbantartható. Példaként képzeljünk el egy egyszerűsített szituációt, amiben adott több
View, amelyek azonos modellt használnak, amelyik modellen van egy dátummező definiálva. Egyszer
csak az az igény merül fel a felhasználótól, hogy ez a mező pirossal jelenjen meg, ha a dátum értéke
régebbi mint a mai nap. Ha ezt a feltételt a modellben értékeljük ki, akkor az összes View-t egy helyről
ki tudjuk szolgálni. Igen ezt a pirosítást más módon is meg lehet csinálni, de ha bővül a feltétel, mondjuk
azzal, hogy csak akkor kell pirosnak lennie, ha régebbi, mint a mai nap és egy másik mező nincs kitöltve
és a Karcsi még nem hagyta jóvá és egyébként még jogom is van kitölteni és ... Gondolom érzékelhető,
hogy gyorsan változó, kikristályosodó üzleti igények mellett komoly létjogosultsága van ennek a
kiértékelési megközelítésnek.
Az előző megközelítés nagyjából megáll a megjelenítést szabályzó (általában pár soros) metódusok,
propertyk implementálásánál vagy a propertyk attribútumos dekorálásánál. A most tárgyalt
megközelítés azt mondja, hogy ha már úgy is valahol implementálni kell a nagybetűs Üzleti Logikát,
akkor tegyük bele a modellbe azt is. Még akár bele is férhet a modellbe és akkor egy helyen van
minden, ahogy az OOP logikája adja, az adat és a hozzá tartozó feldolgozás is. A kontroller is
megszabadulhat a sok kódtól. Azonban óvatosan építsünk ilyen modelleket! A probléma a
modellekben implementált kódok függőségei. Tartsuk szem előtt, hogy a modellosztályt az MVC
leginkább egy önmagában érvényes adatblokként kezeli és értelmezi. Ha magával hurcol más üzleti
logikákat tartalmazó osztályokat, adatlistákat, akkor felesleges inicializálások, vagy éppen
inicializálatlan osztályok jöhetnek létre. Ezért kis rendszereknél még elmegy az üzleti logikát,
számításokat, workflow-kat hordozó modell. Nagy rendszereknél, ahol a mi (kis) MVC projektünket
súlyos szolgáltatások látják el adatokkal és azok végzik az üzleti igény megvalósítását, hát itt azt
mondanám, hogy nincs létjogosultsága. Bár minden helyzet egyedi. Ha a távlati tervben szóba jöhet
akár csak egy kis gondolatfoszlány formájában is, hogy szolgáltatás alapú architektúrát használjunk,
4.2 Modell - Modell és kód 1-51
akkor kerüljük, hogy az MVC modellbe komoly kódot helyezzünk el. Ilyen Service Oriented
Architecture17 esetben a kontrollerek kódjára is vonatkozik ez a javaslat.
Ha mégis ide tervezzük az üzleti kódokat, akkor készítsünk hozzá valami olyan háttér infrastruktúrát,
ami tervezési minták (repository, factory) alapján egységes hidat képez az adatforrás, a
modellosztályok és az osztályokon definiált üzleti logika futtatása között, interakcióban az MVC
infrastruktúrájával. Másként nagyon kusza kódot fogunk kapni.
Összefoglalásként azt tudnám javasolni, hogy a modellt ne terheljük túl kóddal. Egy egyszerű választási
sablon lehet, hogy
Hogy kinek mi a sok kód, az leginkább tapasztalat kérdése. Ezek csak iránymutatások voltak.
Mivel a modell egy osztály, kézenfekvőnek tűnik, hogy a modell kezdeti belső értékeit, a konstruktorból
állítsuk be. Nagyon fontos megjegyezni, hogy a modell konstruktorába maximum olyan inicializáló
kódot érdemes tenni, ami annyit teljesít, hogy a modell felhasználásakor a null reference
exceptionöket el tudjuk kerülni. A felesleges alapbeállításokat is érdemes elkerülni (int típusnak 0
érték, booleannak false, stb). Olyan kódot soha ne tegyünk a paraméter nélküli konstruktorba, ami
adatbázisból vagy fájlból adatokat olvas, erőforrásokat nyit meg. Egyébként sem szép, de a modellnél
messze kerülendő. Jellemzően a modellben tárolt listboxok és comboboxok elemlistáit szokták így
(helytelenül) feltölteni. Az óvatosság oka az, hogy a modellünket az MVC belső infrastruktúrája is tudja
példányosítani (model binder) néha feleslegesen is, ami óriási overheadet visz a működésbe, ha
ilyenkor terjedelmes és lassú konstruktorkódnak kell lefutnia. Minek töltenénk fel a combobox
elemlistáját, ha nem is használjuk fel a beérkező post request esetén? Célszerű a modellt, egy külön
beállító metóduson keresztül feltölteni alapadatokkal (Setup, Init) ahogy más hagyományos osztálynál
is, vagy fenntartani egy paraméteres konstruktort erre a célra. A framework működéséből
következően, a modellt a kimenő HTTP válasszal kapcsolatban kell feltölteni adatokkal, amikor amúgy
is a saját kódunk felügyelete alatt van a modell példányosítása és alapbeállítása. Ekkor azt csinálunk
vele, amit akarunk.
Ugyan ez az ajánlás vonatkozik a mezőinicializálókra is, ha azok valami terjedelmes objektumot akarnak
példányosítani:
17
http://en.wikipedia.org/wiki/Service-oriented_architecture
4.3 Modell - Modell és jellemzők 1-52
Ahogy volt már szó a 2. példakód ismertetése során, a modellt magát és a modellben levő propertyket
további jellegzetességekkel ruházhatjuk fel. Típusuk és a nevük mellett attribútumokkal jelezhetjük az
MVC keretrendszernek, hogy
Hogyan szeretnénk megjeleníteni az adott property tartalmát. Például egy DateTime típusú
tulajdonságból csak a dátumot vagy csak az időt. Vagy egy számot ezresekre szeretnénk
tagolni. Esetleg nem is szeretnénk, hogy megjelenjen a felületen.
Mi legyen az adatmezőhöz tartozó felirat, címke (label) tartalma.
Az adatot milyen feltételek szerint tarthatjuk érvényesnek, milyen validációs szabályok
érvényesek rá.
Ha a modellünk egyben ORM objektum is, akkor az adat tárolását milyen típusú tábla mezőben
tároljuk.
Egyéb technikai attribútumok
4.3.1. Megjelenés
HiddenInputAttribute
Ezzel dekorált tulajdonság esetében azt tudjuk elérni, hogy annak értéke egy rejtett HTML inputban
(<input type="hidden" />) lesz tárolva, azaz nem fog megjelenni, ha az EditorFor Html helperrel
generáljuk (lesz róla még szó). A hidden mezőket általában arra használjuk, hogy a bennük tárolt érték
egy körutazáson vegyen részt az oldal lekérés és post visszaküldés ciklusban (mi generáltuk és ezt is
szeretnénk visszakapni). A másik gyakori felhasználás, ha a tartalmát kliens oldalon javascriptből
állítjuk össze és azt szeretnénk, hogy a post folyamán ez is elküldésre kerüljön. Példa lehet erre egy
összetett, és emiatt egy darab HTML input mezővel nem lefedhető felhasználói vezérlő. Van egy
érdekes képessége is. Ha így definiáljuk: [HiddenInput(DisplayValue = true)] , akkor megjelenik
a felületen az értéke is, de természetesen nem lesz szerkeszthető.
DisplayAttribute
Akkor van rá szükség, ha a propertyhez szeretnénk egy címkét ragasztani, amit egy HTML <label />-
t fog számunkra megjeleníteni. A label szövege a Name paraméterből származik. Lehet közvetlenül az
a szöveg, amit megadunk a Name-en keresztül:
Vagy a másik lehetőség az, hogy a Name paraméter egy resource fájl elemét jelenti és akkor egy .resx
fájlból fog érkezni a megjelenítendő szöveg. Ekkor definiálni kell a ResourceType-on keresztül azt a
resource típust (a .resx fájlból automatikusan generált osztály), amiben a Name által meghatározott
publikus tulajdonság szerepel.
Fontos, hogy publikus legyen a metódus, mivel ezt majd az MVC keretrendszer fogja felhasználni és
nem a mi alkalmazásunk. Ahhoz hogy a resource definíciónk elérhető legyen az MVC számára, a
resource generátort értesíteni kell, hogy publikus metódusokat hozzon létre a resource adatok típusos
elérését biztosító .Designer.cs háttérfájlban. Ha létrehozunk egy új ’UILables.resx’ fájlt, akkor ezt itt
kell beállítani:
A resource fájlokkal
többnyelvű megjelenítést,
így jelen esetben
többnyelvű mezőfeliratot is
lehet készíteni. Erről egy
külön fejezet fog szólni.
DisplayNameAttribute
Ez a DisplayAttribute régebbi verziója, amivel csak a címke feliratot tudjuk statikusan megadni. Nincs
lehetőség resource fájl elem kapcsolására. Nem érdemes használni csak azért említettem meg, hogy
ne keverjük az előzővel, mert nem ugyanaz.
UIHintAttribute
Editor és Display sablont határoz meg a DisplayFor és az EditorFor Html helperekhez. Részletesen a
View fejezetben tárgyaljuk, mert további részletek ismerete szükséges a használatához.
DataTypeAttribute
18
A validációt a böngésző biztosítja és nem az MVC vagy JS kód, ha csak a DataType alap attribútumot használjuk.
4.3 Modell - Modell és jellemzők 1-54
DisplayFormatAttribute
Ha azonban ezt az oldalt visszaküldjük (submit) az Edit actionnak, azt fogja mondani, hogy nem jó:
4.3 Modell - Modell és jellemzők 1-55
Ez furcsa, nem? Hisz minden jó. Csak annyit kértem, hogy pénznemben szeretném megadni az értéket.
Ezért van az, hogy az alapértelmezett működés az, hogy a szerkesztő mezők nem formázottak, mert
ilyen és sok hasonló meglepetésben lesz részünk. Az ilyen helyzetekre nincs felkészítve az MVC.
Hasonló problémák előkerülhetnek, ha dátumokkal dolgozunk. Azonban miután kitöröljük a szám után
a ’Ft’–ot, akkor minden rendben fog lezajlani. A lokalizált adatokkal általában gond szokott lenni, de
meg van a lehetőségünk, hogy az MVC viselkedését megváltoztassuk.
Lehetőségek DotNet Framework 4.0 esetében További attribútumok DotNet 4.5 alatt
ValidationAttribute ValidationAttribute
CompareAttribute (System.Web.Mvc) CompareAttribute (DataAnnotations)
CustomValidationAttribute MaxLengthAttribute
DataTypeAttribute MinLengthAttribute
EnumDataTypeAttribute MembershipPasswordAttribute
RangeAttribute DataTypeAttribute
RegularExpressionAttribute CreditCardAttribute (+Microsoft.Web.Mvc)
RequiredAttribute EmailAddressAttribute (+Microsoft.Web.Mvc)
StringLengthAttribute FileExtensionsAttribute (+Microsoft.Web.Mvc)
RemoteAttribute PhoneAttribute
UrlAttribute (+Microsoft.Web.Mvc)
AccessAttribute
Ha a fentiek nem elégségesessek az igényeink lefedésére, akkor nekiláthatunk, hogy saját validációs
attribútumot írjunk. Erről is fog szólni, az bizonyos 9.5 fejezet.
RequiredAttribute
Ennek az attribútumnak nincsenek validációs paraméterei. Azzal, hogy ráillesztjük a propertyre az jelzi,
hogy kell valami érték. Megjegyzendő dolog, hogy az olyan típusú propertykre hiába rakjuk rá, amelyek
nem nullázhatóak. Mivel az Int32 alapértelmezett értéke 0, teljesen elfogadható mint kötelező érték.
Hasonlóan a false a booleannál és az 0000.00.00 mint dátum. A string és a nullable típusoknál van
értelme a Required attribútumnak. Használatára - hasonlóan a DisplayAttribute-nál látottakhoz - két
lehetőség is van. Az egyik amikor a modellbe ’égetem’ a hibaüzenetet, ami egynyelvű alkalmazásnál
jöhet csak szóba:
4.3 Modell - Modell és jellemzők 1-56
[Required(ErrorMessageResourceName = "UserNameRule",
ErrorMessageResourceType = typeof(Resources.Validations))]
Mindkét esetben használható egy 0. indexű string formázási helyőrző, ami helyére az aktuális property
neve vagy display neve kerül (DisplayAttribute). Ha nem adunk meg ErrorMessage-t, akkor egy
alapértelmezett angol üzenet fog megjelenni, ahogy azt a 1. ábra mutatta. Ez az üzenet megadási
lehetőség, az összes további validációs attribútumon is használható.
StringLengthAttribute
MaxLengthAttribute és MinLengthAttribute
Az Array és a String típusú propertyvel érdemes használni. A felületen való validációnál az Array-nak
sok értelme nincs, így a használata az MVC szempontjából nagyon hasonló a StringLengthAttribute
használatához, azzal a különbséggel, hogy ezekkel elég megadni csak az egyik szélsőértéket.
RangeAttribute
[Range(100.1, 200.1)]
public decimal TotalSum { get; set; }
DataTypeAttribute
Validációs hibát nem okoz, de a megadott adat, mint idő nem fog visszajönni a mentés után az
ellentmondásos attribútumok miatt.
Még egyszer kiemelném: Egy DataType(DataType.EmailAddress) nem ír elő email validációs szabályt
az MVC számára, kizárólag a leszármazottak definiálják. Jogos kérdés, hogy akkor meg hogy lehet az,
hogy a jobb oldali kép mégis azt mutatja, hogy hibás
email cím esetén validációs üzenetet kapunk? Ha
megnézzük a generált HTML kódot, ezt a sort
találjuk az email mezővel kapcsolatban:
A vastagon kiemelt definíció szerint a validációt a böngésző biztosítja, de csak HTML 5 esetén, mivel a
fenti type="email" definíció csak innentől érhető el. Ezek a DataType attribútum definíciók generálnak
új HTML 5 új beviteli mezőtípusokat, amiknek a validálása a böngészőre van bízva, ha mi más validációt
nem írunk elő:
[DataType(DataType.Url)]
[DataType(DataType.EmailAddress)]
[DataType(DataType.PhoneNumber)]
[DataType(DataType.DateTime)]
[DataType(DataType.Date)]
[DataType(DataType.Time)]
[DataType(DataType.Url)]
A valódi, MVC által kezelt validációs megoldásokat a DataType attribútum leszármazottjai biztosítják.
A .Net 4.0 alatt csak az EnumDataTypeAttribute érhető el, de .Net 4.5 alatt van több ilyen leszármazott
is. Az attribútumok funkciói a nevük alapján szerintem kitalálhatók, és az is hogy milyen validálási
szabályt írnak elő.
A FileExtensions attribútumról annyit azért érdemes tudni, hogy a böngészőben, a feltöltésre szánt
fájlok kiterjesztését lehet vele meghatározni. A CreditCardAttribute, EmailAddressAttribute,
FileExtensionsAttribute, UrlAttribute attribútumok az MVC Futures nevű projektből kerültek át a .Net 4.5-
be. Az MVC Futures használható a .Net 4.0 alatt is. Emiatt az előbb felsorolt attribútumok is elérhetők
azon keresztül. Az MVC Futures jelenleg az "Mvc4Futures" nevű NuGet csomagban érhető el. Ennek a
kiegészítőnek az assembly neve és a névtere is a Microsoft.Web.Mvc, amit jelez a fejezet elején levő
táblázat.
Az enum értékét a szöveges változata alapján tudja validálni, tehát ha „Retailer”-t írok, elfogadja.
Ha olyan szót adok meg, ami nincs az enum értéklistájában, akkor azt visszadobja, azzal hogy nem jó.
Sajnos ez ennyit tud, pedig elvárható lenne hogy legalább egy legördülő listát adjon, de még jobb az
lenne, ha a legördülő listában az enum Description attribútumban megadott szöveg jelenne meg. A
leges-legjobb pedig az lenne, ha a Description szövege is Resource fájlból tudna jönni. Úgy látszik az
enum egy mostohagyerek. (Az Entity Framework is csak az 5-ös verziója óta kezeli natívan).
CompareAttribute
Két property mező tartalmát hasonlítja össze. Akkor érvényes a validáció, ha a kettő egyforma. Az
összehasonlításra az object.Equals() metódust használja. A legjobb példa erre a Visual Studio projekt
template által generált LocalPasswordModel.
[DataType(DataType.Password)]
[Display(Name = "New password")]
public string NewPassword { get; set; }
[DataType(DataType.Password)]
[Display(Name = "Confirm new password")]
[Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")]
public string ConfirmPassword { get; set; }
Az attribútum elnevezése szerintem zavaró. Jobb lett volna egy EqualsAttribute név, végül is csak
egyenlőség vizsgálatára jó. A Compare név számomra azt sugallja, hogy egy paraméterrel megadhatok
egy relációs műveletet (<, >, <=, >=, !=) a propertyk értékeinek összehasonlításához, de erre nincs
lehetőség. Ezzel csak az akartam hangsúlyozni, hogy az MVC-be és a DataAnnotations-be sincs
beépítve, igazi komparátor attribútum.
RemoteAttribute
Ennek segítségével úgy oldhatjuk meg a böngésző oldali validációt, hogy a validálandó inputmező
tartalma egy kontroller actionben értékelődik ki. Normál esetben a kliens oldali validációt a
böngészőben futó JS kódban kell implementálni. Itt most nem készítettem példát, mert annál
összetettebb dologról van szó, viszont később megnézzük részletesen egy külön alfejezetben.
RegularExpressionAttribute
Amit meglehet fogalmazni regular expression-nel, azt fel tudjuk használni validációra is. A példában a
FullName mezőbe csak betűket és szóközt tartalmazó szöveget lehet megadni, amelynek hossza 1 és
20 között van.
[RegularExpression(@"^[a-zA-Z'\s]{1,20}$")]
public string FullName { get; set; }
Ennek az ErrorMessage paraméterét mindenképpen töltsük ki, még akkor is ha angol nyelvű, végtelen
türelmű, vak ügyfél számára fejlesztünk, ugyanis az alapértelmezett üzenettől biztos hanyatt vágja
magát: The field Felhasználó név must match the regular expression '^[a-zA-Z'\s]{1,20}$'
Nem mellékesen a .Net 4.0 alól hiányzó email validációs attribútum, egy jól megfogalmazott reguláris
kifejezéssel pótolható. Amúgy a .Net 4.5 alatt ezt szintén regex-el oldják meg.
A reguláris kifejezések validációs helyzetek lefedésére számtalan példa kering a neten. Azonban
legyünk nagyon figyelmesek, mivel a relatív kislélekszámban beszélt magyar nyelvre (és más nyelvekre
is, amik nem csak az angol abc szűk készletére korlátozzák a karakterkészletüket) nincsenek tekintettel
az angolszász, csípőből lökött példák. Például azt a tényt, hogy egy domain név már tartalmazhat
ékezetes karaktereket (ami egy URL-ben vagy email címben is megjelenhet) sok példa nem veszi
figyelembe.
AllowHtmlAttribute
Alapértelmezetten az MVC nem engedi, hogy a propertybe érkező szövegben HTML markup legyen.
Ezzel az attribútummal be tudjuk engedni a HTML tartalmat. Hatására a háttérben le lesz tiltva az un.
4.3 Modell - Modell és jellemzők 1-60
request validation, a beérkező kérés feldolgozása alatt az adott propertyre. Majd az action filtereknél
látni fogjuk, hogy ezzel még nem teljes az engedély, mert ezt az action szinten is meg kell engedni a
ValidateInputAttribute(false) segítségével .
CustomValidationAttribute
[CustomValidation(typeof(ValidationDemoModel), "ValidateFullName")]
public string FullName { get; set; }
Az attribútum első paramétere egy osztálytípus. A második az osztály egy publikus statikus
metódusának neve szövegesen, amely metódus fogja végezni a validációt és egy darab (!) 19
ValidationResult objektummal kell visszatérnie. A validációt végző osztálytípus most az egyszerűség
kedvéért a modell maga, de lehetne egy külső osztály is.
[CustomValidation(typeof(ValidationDemoModel), "ValidateDemoModel")]
public class ValidationDemoModel
{
// A normál modellpropertyk
public static ValidationResult ValidateDemoModel(ValidationDemoModel tovalidate)
{
if (string.IsNullOrEmpty(tovalidate.Address) && string.IsNullOrEmpty(tovalidate.Email))
return new ValidationResult("A címet vagy az email címet meg kell adni!");
return ValidationResult.Success;
}
}
Az előbb nem említettem, de a validátor metódus paraméter típusának meg kell egyeznie azzal a
típussal, amire a CustomValidation attribútumot tettük. Ezért itt most a paraméter típusa nem string,
19
Ez egy korlát, amit majd a validációkkal foglalkozó fejezetben ledöntünk.
4.3 Modell - Modell és jellemzők 1-61
hanem maga a validálandó osztály. A logika implementálása egyetlen sor, ezért nem lenne érdemes
egy külön ValidationAttribute leszármazottat írni (bár azt is megfogjuk majd próbálni).
return new ValidationResult("A címet vagy az email címet meg kell adni!",
new[] { "Address", "Email" });
Sajnos, azonban ez ebben a helyzetben a CustomValidation-nel nem működik. Máshol az MVC-ben (és
más platformon is) igen, amit majd a validációkat részletező fejezetben meg is nézünk.
Ide tartozik, hogy az osztályszintű validáció kisebb prioritású, mint a tulajdonság szintű. Ezért addig,
míg az egyedileg szabályozott propertyk közül egy vagy több értéket hibásan adunk meg és ezek
mellett mind megjelennek a hibaüzenetek, addig az osztályszintű custom validáció ki sem fog
értékelődni, míg a property szintű, validációk által felügyelt mezőket nem javítjuk ki.
A sorrend:
Az egyes propertykre több validációs attribútumot is rakhatunk. Ilyenkor az első hibás validáció esetén
a továbbiak nem fognak kiértékelődni és ilyen helyzetben - nem úgy, mint a property-osztály
viszonylatban - a CustomValidation-nak elsőbbsége van. Másként fogalmazva, a propertyn levő
CustomValidation fog kiértékelődni elsőként, az modellosztályon definiálva viszont a propertyken levő
validációs attribútum(ok) után.
4.3 Modell - Modell és jellemzők 1-62
Idemásoltam, hogy látható legyen, eddig milyen attribútumok kerültek szóba. Némelyik ki van
kommentezve, mert értelmetlen lenne ha azokat is engednénk érvényesülni. Pl. Nem lehet két
Required attribútum egy propertyn.
[CustomValidation(typeof(ValidationDemoModel), "ValidateDemoModel")]
public class ValidationDemoModel
{
[HiddenInput(DisplayValue = false)]
public int Id { get; set; }
if (!datalist.ContainsKey(id))
{
datalist.Add(id, new ValidationDemoModel()
{
Id = id,
FullName = "Tanuló " + id,
Address = string.Format("Budapest {0}. kerület", id + 1),
Email = "proba@proba.hu",
TotalSum = id * 2345.45m,
LastPurchaseDate = DateTime.Now.AddDays(-2 * id)
});
}
return datalist[id];
}
if (string.IsNullOrWhiteSpace(fullName))
return new ValidationResult("A nevet meg kell adni!");
if (fullName.IndexOfAny("0123456789".ToCharArray()) >= 0)
return new ValidationResult("A név nem tartalmazhat számot!");
return ValidationResult.Success;
}
Hacsak nem valami egészen speciális MVC alkalmazást készítünk, szükségünk lesz arra, hogy a
modellünk tartalmát eltároljuk hosszabb időre. Szintén általános jellemző, hogy a modell
tartalmát, meglévő adatbázis adatokból állítjuk össze. A típusos modellünkben levő adatok
(adatbázisban) tárolását szokták perzisztálásnak nevezni. Ahhoz, hogy a modelladatok
perzisztensek legyenek, szükség van egy adatkezelési rétegre, ami elvégzi az adatforrásból érkező
adatok és a modellosztály propertyjeiben tárolt adatok közti transzformációt. Más szóval az
adatbázis mezőket megfelelteti a modell propertyjeivel. A megfeleltetés során gyakran
típuskonverziót is kell végezni. Ezeket a műveleteket elvégző eszközöket Object-Relational-
Mapper-ek végzik el. (ORM20). Mostanra több ilyen ORM létezik a .Net környezetben: NHibernate,
Entity Framework (Microsoft), OpenAccess ORM (Telerik), XPO (Devexpress), LLBLgenPro, hogy
csak azokat soroljam fel, amikkel már volt kisebb-nagyobb tapasztalatom. Némelyik ingyenes,
némelyikért fizetni kell. A legtöbbjük többféle adatbáziskiszolgálóval képes működni. Az hogy
Oracle, MSSQL, SQLite, Access vagy más adatbáziskezelőhöz kapcsolódnak az nekik közel mindegy.
Legalább két fontos előnye van, ha ilyen ORM-et használunk. Egyrészt pár kattintással előállítható
a modell az adatbázisból vagy egy XML-ben meghatározott sémából, vagy fordítva, az adatbázis
állítható elő a modell kódjából (ahogy láttuk a 3.8 fejezetben), vagy XML-ből. A másik előny, hogy
a validációs adatok és display nevek, szintén előállíthatóak az adatbázis séma alapján. Ez utóbbi
nagyon kényelmes, de néha hátrányos is lehet, ha például az automatikusan generált property
attribútumok nem úgy állnak össze, ahogy a mi speciális igényünknek megfelel. Ilyenkor jön a
modellreszelés (finomhangolás) a partial osztályok és más rafinált lehetőségek.
20
http://en.wikipedia.org/wiki/List_of_object-relational_mapping_software
4.4 Modell - Modell és tárolás. Adatperzisztencia 1-64
11. ábra
Ami erre a modell megvalósítási típusnál célravezető, hogy ne legyen szoros kapcsolatban az
adatkezelési réteg egyik összetevőjével sem. Teljesen le legyen választva, és ne legyen hivatkozása az
adatkontextusra (pl. UnitOfWork, DataSet, DbContext, Session). Az adatkontextus alatt olyan
infrastruktúrát szoktak érteni, ami a külső adatszolgáltatóhoz kapcsolódva (pl. adatbázis szerver)
összefüggő adatokat szolgáltat az alkalmazás felsőbb rétegei számára és adatokat fogad a felsőbb
rétegtől, amiket az adatszolgáltatónak továbbít. Ezeket egységben kezeli, pl. táblákat és köztük levő
relációkat vagy objektumokat és ezek közti referenciákat. Felületet biztosít a szokásos
adatműveletekre (CRUD), mint pl. create, retrieve, update, delete, tranzakció kezelés, lapozás. A
hátrány az MVC modell szempontjából, hogy az ORM-ek közül némelyik állapotfüggő
segédinformációkat helyez el az általa felügyelt osztályokban, így az modellünkben is. Így ezekkel a
session/context adatokkal magukhoz láncolják azokat. Nagyon jellemző, hogy az ilyen adatkontextusok
IDisposabe interfész alapúak, és elvárják hogy tényleg le is kezeljük annak Dispose igényét. Sajnos az
MVC-nek még akkor is szüksége van a modellre, amikor már átkerült a végrehajtás az MVC
infrastruktúrájába, és a Dispose metódust nem tudjuk meghívni. Ezt is érdemes szem előtt tartani az
ORM kiválasztásánál és használatánál. Ahogy már láttuk az Entity Framework nagyon jól használható
az MVC-vel, mert az adatmodell osztályai nem függenek az EF adatkontextusától (DbContext)
Egy másik ok, hogy ne legyen szoros kapcsolat az adatkezelési réteggel az, hogy az MVC infrastruktúrája
is tudja példányosítani számunkra a modellt (model binder). Ez nagyon kényelmes tud lenni, de ha az
ORM nem alkalmas rá, akkor fájdalmassá válik a használata.
A legalkalmasabb ebben az esetben egy olyan modellosztály, ami nem öröklődik más osztályból,
nincsenek függőségei, csak minimális kód van benne, és a propertyjei pontosan megfeleltethetőek az
adatbázis mezőinek. Konkrétan a nevük is egyezik és a típusuk is (már amennyire lehetséges). Szokták
az ilyen mindenre használható, független, nyers modellobjektumot POCO-nak nevezni, de nem azért
mert olyan kicsi, mint egy pocok, hanem az angol rövidítés szerint: Plain-Old-CLR-Object. Egyes ORM-
ek jól támogatják a POCO objektumok kezelését, mint például az NHibernate, Entity Framework 4-től.
Másokról ezt nem lehet elmondani, mert „túlsúlyos” osztályokat igényelnek (XPO) olyanokat, amik
kötelezően leszármazottak. Ha a modellünk erősen függ az ORM-től és valami kötelező ősosztályból is
kell származtatni, akkor a modell feldolgozása során el fog következni a pillanat, amikor a modellt úgy
4.4 Modell - Modell és tárolás. Adatperzisztencia 1-65
kell beállítani, esetleg klónozni, hogy az megfeleljen az ORM igényeinek. Ez pedig jelentős
többletmunkával jár.
További jellemzője ennek a modellmegvalósításnak, hogy mivel ennyire adatbázis vagy tárolásközeliek
a modellosztályok, így a tárolással kapcsolatos jellemzőket is magával hordozhatják. Például:
21
http://msdn.microsoft.com/en-us/library/ff649690.aspx
4.4 Modell - Modell és tárolás. Adatperzisztencia 1-66
Az adatbázisban tárolt adatok, a normalizálás miatt szeparált táblákban tárolódnak és a táblák között
relációk vannak. A lekérdezés sorai gyakran többszörös joinnal vannak leválogatva, hogy a szükséges
összetartozó adatokat egyben tudjuk felhasználni. Ezeket az adatbázisban nézetekként (szintén View)
tudjuk definiálni. Ennek az analógiája ez a nézet jellegű modellforma. Ezért sok esetben a modell nem
is szokott más lenni, mint egy adatbázisnézet vagy egy összetett select adatai, objektumba csomagolva.
Ebben a megközelítésben a modell csak nagyvonalakban hasonlít az adatbázis sémájához. Emiatt lehet,
hogy sok manuálisan megírt osztály-osztály mappelésre lesz szükségünk akkor, amikor a felhasználó az
adatokat módosítja, és szeretnénk ezeket update-elni az adatbázisban. Ha az egyik osztályunk a
modell, a másik pedig az adatbázis lekérdezés vagy View sémájából képzett osztály, akkor látható, hogy
nem lesz egyszerű munkánk. Erre léteznek auto mapper-ek, amik segítségével konfigurálhatóan tudjuk
megtenni ezeket az összerendeléseket. Ha az adatbázistáblának a sémája változik, akkor a modellt is
rendszeresen aktualizálnunk kell.
Nagyon hasznosak tudnak lenni a nézet jellegű modellek, a kliens oldali működés hatékony
kiszolgálásában:
ha böngészőben szeretnénk validálni, mielőtt a felhasználó elküldené az adatait.
ha a kliensoldali javascript kódunkat kell kiszolgálni JSON adatokkal.
fogadni kell JSON adatokat.
Szolgáltatás alapú architektúrában gondolkozunk, és a szolgáltatás is ilyen adatmodellekkel
operál.
A hátrányuk, hogyha nem találunk rá valami automatizmust (pl. T4 template-et), akkor sok
többletmunkát okozhatnak az osztály-osztály transzformációk.
Ilyen modellt, igen gyakran valami részfunkció kiszolgálására írunk, aminek valószínűleg közvetlenül
kevés köze van az adatbázishoz. A bejelentkezést (felhasználó név-jelszó) vagy a jelszóváltozatást (régi
jelszó, kétszer az új jelszó) kiszolgáló View-k modelljei is ilyenek, a VS MVC Internet projekt template
által létrehozott alkalmazásban.
Kicsit haladjunk tovább és képzeljünk el nagy rendszereket, ahol az MVC kontrollereink semmilyen
kapcsolatban sem állnak az adatbázissal, se ORM, se repository nincs az MVC projektünkben. Viszont
vannak szolgáltatáshívások (service metódusok), amiknek van egy definíciós sémája, ami leírja a
meghívható szolgáltatásokat, nevük és paraméterük alapján, valamint a szolgáltatás által küldött-
fogadott adatok szerkezetét, típusát. Az utóbbi időben igencsak el vagyunk látva minden jóval, hogy
szolgáltatásokat építsünk és használjunk, mégsem emelném ki egyiket sem. Ami közös jellemzőjük,
hogy hálózati forgalmat generálnak, amiből a kevés is sok és lassú, hacsak nem egy gépen vannak a mi
MVC alkalmazásunkkal. Emiatt érdemes a szolgáltatás felületét és az adatait (amit osztályokon
keresztül típusosan tudunk felhasználni) úgy megtervezni, hogy az egy darab szolgáltatáshívással
kielégítse az adatigényünket, amennyire csak lehet. A szolgáltatás felületi adatdefiníciója nagyon jól
meg tudja valósítani az MVC modellel támasztott igényeinket. Ezzel a szolgáltatás alapú felépítéssel a
kontrollerünk kódját is minimalizálni tudjuk. A modell ilyen esetben egyfajta adattranszfer szerepet
kap. Egyrészt ezen keresztül történhet az adatok átvitele a szolgáltatás felé, másrészt a kontroller és a
View között is. Mivel a modellosztályt így két technológia is használni fogja, ezért célszerű a komplex,
összetett osztályokat elkerülni. Szokták az ilyen modelleket DataTransferObject-nek is nevezni, vagy
csak a DTO rövidítéssel hivatkoznak rá. A hálózati forgalommal való takarékosság jegyében ezek az
osztályok csak annyi propertyt tartalmaznak, amik kielégítik, lefedik a konkrét helyzet adatigényét.
Abban az esetben, ha a modelljeink mind, vagy legalábbis a jelentős része meglévő (pl. adatbázis) séma
szerint épül fel, akkor kicsit gondban leszünk a property szintű attribútumok használatával. Ilyen
helyzetet tudnak okozni az ORM-ek és a szolgáltatások, amikor is a számukra generált osztályokkal kell
dolgozni. Ilyenkor a modellosztályunkat az adatbázis vagy valamilyen XML, WSDL fájlban
meghatározott séma szerint generáltatjuk a Visual Studio-val, vagy más eszközzel. Ezt a generálást
időről-időre, ahogy változik az adatbázis ORM struktúra vagy a service definíciója, újra le kell futtatni.
Aminek az lenne az eredménye, hogy a generált fájlba a propertykre manuálisan helyezett
attribútumok elvesznek, ha ilyet megpróbálnánk. Megpróbálhatjuk belerakni a kódgenerátorba az
attribútumokat, de általában az fix sémadefiníció nem tartalmazza annyira részletesen jól kifejtve a
validációs és egyéb szabályokat legfeljebb azokat, amik a perzisztencia vagy a kapcsolat vezérlésére
használatosak. Általában csak olyan jellemzőket, mint a mezőben tárolható karakterek mennyiségét,
DB adattípust, és egyéb technikai jellemzőket (melyik a primary key, melyik a timestamp). Így a
generált modell nem fogja tartalmazni a helyes validációt. Hogyan tudjuk mégis kiegészíteni a
modellünket és a propertyket attribútumokkal úgy, hogy ne függjünk az automatikusan generált
osztálytól?
MetadataTypeAttribute
Ezt a problémát egy un. ’buddy class’-al tudjuk frappánsan megoldani. Szükségünk van egy
MetadataType attribútummal dekorált partial class-ra, az automatikusan generált (ORM) osztály
mellé. Az ORM osztálygenerátorok általános jellemzői, hogy nyitva hagyják a bővíthetőséget és
4.6 Modell - Egyéb modellattribútumok 1-68
részleges osztályokat (partial class) hoznak létre a generált típusok definíciójában. A MetadataType egy
típust vár, ez a buddy class, ami azonos nevű és típusú propertyket tartalmaz mint a generált osztály.
Ezekre a propertykre ráaggatott validációs attribútumokat úgy fogja értelmezni az MVC framework,
mintha az eredeti generált ORM osztály azonos nevű tulajdonságain lennének.
A fenti osztály partial párja látható a 6. példakódban a felső részen, amiben az osztály megkapta a
MetadataType attribútum paraméterében a PersonMetadata osztály típusát. Másra itt nincs szükség.
A PersonMetadata osztályban pedig az azonos nevű és típusú FirstName propertyre akasztottam rá a
Required és a Display attribútumokat.
[MetadataType(typeof(PersonMetadata))]
public partial class Person {
}
[Required]
[Display(Name = "First Name")]
public string FirstName {get;set;}
}
6. példakód
Ezt a metodikát érdemes alaposan megérteni, mert később még használni fogjuk. Persze nincs szükség
erre a trükkre, ha a modellosztályokat mi határozzuk meg és az osztályok alapján készítjük vagy
készítetjük el az adatbázis sémát. Úgy tűnik, hogy ez utóbbi az un. ’code-first’ megközelítés, jóval
használhatóbb az MVC-vel kapcsolatban, ha ORM-ről van szó. Nem is értem miért kellett ezzel ennyit
várni az Entity Framework esetében.
ScaffoldColumn
Előfordulhat, netán ideiglenesen, hogy egy model propertyt mégsem szeretnénk megjeleníteni. Ha
ezzel az attribútummal láttuk el a propertyt, egyszerűen nem fognak létrejönni a HTML markupok.
Akkor sem, ha a View-ban ott vannak a Html helperek. Ha a modellhez nem készítünk View-t vagy más
sablont, az MVC lehetőséget ad az EditorForModel vagy a DisplayForModel Html helperekkel, hogy
dinamikusan generált View sablont használjunk. Mivel ilyenkor a Html helpereket nem mi írjuk a View-
ba, a ScaffoldColumn-al még megvan a lehetőségünk, hogy letiltsuk a property alapértelmezett
megjelenítőjét vagy szerkesztőjét.
A beérkező post request form elemeit és JSON adatát típusos modellként tudjuk átvenni az MVC-től.
Az ilyen model propertyjeinek az automatikus feltöltésekor van tiltó-engedélyező szerepük ezeknek az
attribútumoknak. Az Editable és a ReadOnly egymásnak a fordítottjai. Az Editable-nak elsőbbsége van,
4.7 Modell - Egy demó modell 1-69
amúgy nem érdemes a kettőt egy propertyn használni. Ezekről később a 8.1 fejezetben lesz részletesen
szó.
Ahhoz, hogy a további részekben ki tudjuk próbálni a lehetőségeket, szükségünk lesz egy fapados
modellre.
using System;
using System.Collections.Generic;
using System.Linq;
using System.ComponentModel.DataAnnotations;
using System.Web.Mvc;
namespace MvcApplication1.Models
{
public class ActionDemoModel
{
[HiddenInput(DisplayValue = true)]
public int Id { get; set; }
//[AllowHtml]
[Display(Name = "Vásárló címe")]
//[DataType(DataType.MultilineText)]
public string Address { get; set; }
if (!datalist.ContainsKey(id))
{
var products=ActionDemoProductModel.CreateProducts();
datalist.Add(id, new ActionDemoModel
{
Id = id,
FullName = "Tanuló " + id,
Address = string.Format("Budapest {0}. kerület", id + 1),
Email = "proba@proba.hu",
TotalSum = id * 345.45m,
LastPurchaseDate = DateTime.Now.AddDays(-2 * id),
PurchasesList = products,
KeyPurchase = products[2]
});
4.7 Modell - Egy demó modell 1-70
}
return datalist[id];
}
[Display(Name = "Cikkszám")]
public string ItemNo { get; set; }
[Display(Name = "Mennyiség")]
public int Quantity { get; set; }
#region Listafeltöltés
private static int tid; //next id
public static IList<ActionDemoProductModel> CreateProducts()
{
var rand = new Random();
int count = rand.Next(5, 10);
var result = new List<ActionDemoProductModel>(count);
for (int i = 0; i < count; i++)
{
result.Add(new ActionDemoProductModel
{
Id = ++tid,
ItemNo = string.Format("szam{0}-k{1}", i, DateTime.Today.Day),
Quantity = rand.Next(1, 1000),
ProductName = string.Format("{0}{1}",
ProductNames[rand.Next(ProductNames.Length)], tid * 1001)
});
}
return result;
}
private static readonly string[] ProductNames =
new[] { "Szék", "Ágy", "Asztal", "Párna", "Tükör", "Polc" };
#endregion
}
}
7. példakód
Ez a modell egy minimalista tárolóosztály. A GetModell statikus metódus a datalist szótárból visszaadja
az id paraméternek megfelelő modell példányt. Ha nincs ilyen, akkor példányosít egyet,
felparaméterezi, és azt adja vissza. Ezzel meg is valósítottunk egy butus tárolót.
5.1 A kontroller és környezete - Az alkalmazásunk beállítása. A web.config 1-71
5. A kontroller és környezete
Az, hogy az MVC alkotói ilyen lazán csatolt megoldást választottak, lehetőséget és irányelvet
teremtettek arra, hogy az implementált metódusok és osztályok ne függjenek erősen a
keretrendszertől, mint futtató környezettől. Ezzel nyitva hagyták a lehetőséget, hogy a kódok unit
tesztelhetőek legyenek, és hogy a keretrendszert kiegészítsük, vagy funkcióit lecseréljük anélkül, hogy
ezzel a rendszer más részeinek a működését megzavarnánk. Bárcsak minden keretrendszer ennyire
flexibilis lenne!
A legtöbb webszerver (IIS, Apache, stb.) működését szöveges konfigurációs fájlokkal tudjuk
befolyásolni. Ezekben megadhatjuk, hogy milyen jogosultságellenőrzést szeretnénk, milyen további
bővítő modulokat kívánunk használni, hogyan tudjuk elérni az adatbázis szervert, és további
paraméterekkel részletesen beszabályozhatjuk a webalkalmazás működését.
Nézzük meg a projekt gyökerében levő web.config-ot. Ez egy jól definiált XML fájl. A tartalmát nem
másolom ide, inkább felhívnám a figyelmet arra, hogy a beállítások szekciókra vannak bontva, attól
függően, hogy a webkiszolgálás mely szereplőjére vonatkozik. Igen, a teljes webkiszolgálást testre
szabhatjuk, a webszervert és a mi alkalmazásunkat is. Egy szélsőséges példa, ami a <runtime> ág alatt
szokott lenni, az un. assembly redirekció, amivel előírhatjuk a .Net keretrendszer számára (!), hogy az
esetlegesen igényelt régi verziójú (pl. System.Web.Mvc 2.0.0.0) assembly-k helyett az új 4.0.0.0 verziót
legyen kedves használni. Ez a beavatkozási mélység pedig azt mutatja, hogy a web.config az
alkalmazásunk Achilles-sarka. Nagyon fontos, hogyha beleírunk valamit ebbe, akkor azt meggondoltan
tegyük, főleg ha csapatban dolgozunk vagy az élesben használt alkalmazásról van szó.
Ezeknek a web.config fájloknak van még egy jó tulajdonságuk. Az, hogy a benne foglaltak az adott
mappára és annak almappáira is vonatkoznak (némely kivétellel). Minden almappában elhelyezhetünk
további web.config-ot, amivel kiegészíthetjük vagy felülbírálhatjuk a szülő mappában levő web.config
fájlok beállításait.
Erre az MVC-ben is van példa. A Views almappában van egy másik web.config. Ebben javarészben az
van megfogalmazva, hogy a projekt struktúra Views mappájából nem lehet kiszolgálni semmit, a
tartalma nem hozzáférhető az URL alapján. Próbáljuk meg mit kapunk, ha megpróbáljuk megcélozni a
/Views/Home/About.cshtml fájlt, vagy a /Views/Home/ vagy /Views/ mappát. Ha készítünk egy
index.html fájlt, majd megpróbálhatjuk megnyitni a böngészőből, akkor egy „The resource cannot be
found” üzenet fog érkezni, mivel ennek a mappának a web.config-ja előírja, hogy bármilyen kérést a
5.1 A kontroller és környezete - Az alkalmazásunk beállítása. A web.config 1-72
ravasz HttpNotFoundHandler fog kiszolgálni, tekintet nélkül nemre és korra, aminek a neve is mutatja,
úgy fog csinálni mintha nem lenne ott semmi.
Egy hasznos tanulság következik. Hozzuk létre az előbb említett index.html fájlt a Views/Home alatt
bármilyen értelmes tartalommal. Mondjuk írjuk bele a nevünket, vagy akármit. Ezután kommentezzük
ki a Views alatti web.config fájlban a <httpHandlers> és a <handlers> szakaszokat, pl. így:
<system.web>
<!--<httpHandlers>
<add path="*" verb="*" type="System.Web.HttpNotFoundHandler"/>
</httpHandlers>-->
.
.
.
</system.web>
<system.webServer>
<validation validateIntegratedModeConfiguration="false" />
<!--<handlers>
<remove name="BlockViewHandler"/>
<add name="BlockViewHandler" path="*" verb="*" preCondition="integratedMode"
type="System.Web.HttpNotFoundHandler" />
</handlers>-->
</system.webServer>
8. példakód
A ’. . .’ olyan szakaszt jelöl, ami most nem lényeges, de azért ne töröljük ki onnan, ami gyárilag ott van.
Alkalmazás újraindítás után URL-nek adjuk meg a /View/Home/Index.html és a tartalma meg fog
jelenni. Most nézzük meg a nyitólapot és lám minden jól működik. Miközben még sincs minden
rendben, ugyanis egy nem nyilvánvaló rést ejtettünk az alkalmazás biztonsági beállításán. Tehát fontos,
hogy a web.config-ban vannak olyan szakaszok, amelyeket tényleg nem érdemes piszkálni addig, amíg
pontosan nem tudjuk meg miért is vannak ott. Látszólag nem is okozott problémát, minden működik.
Vajon mikor derülne ki, hogy biztonságilag hibás a web.config, ha így hagynánk?
Ha megnéztük mindkét web.config fájlt, azt gondolhatjuk, hogy nem kell szinte semmit sem beállítani,
hisz alig van benne valami. Valójában az alkalmazásunk gyökerében levő web.config azért ilyen "üres",
mert ez a web.config nem az abszolút értelembe vett root konfiguráció. Ez a konfigurációs fájl
valójában csak a sokadik eleme egy leszármazási láncnak.
A lánc elején áll a Machine.config, ami tartalmazza az összes adott .Net keretrendszer verziót használó
alkalmazás és szerver alapbeállításait. Ez .Net 4 64 bites verzió esetén nálam ezen ez útvonalon volt
elérhető: Windows\Microsoft.NET\Framework64\v4.0.30319\Config\.
A következő konfigurátor fájl az applicationHost.config. Ezt attól függően, hogy normál IIS-t vagy IIS
Expresst használunk, más helyen kell keresni. A fejlesztéssel kapcsolatban most számunkra az Express
a fontosabb. Ennek az elérési útja a felhasználói profilban van:
Érdemes belenézni, mert nagyon sok beállítás itt van meghatározva, amire szükségünk lehet. Talán a
legfontosabb része a Sites felsorolás. Itt vannak azok a webalkalmazások, amiket a Visual Studio-ból
indítottunk el IIS Expresst használva. ("Use Local IIS Web server" beállítás a projekt beállítások
ablakban a Web fül alatt).
<sites>
<site name="WebSite1" id="1" serverAutoStart="true">
<application path="/">
<virtualDirectory path="/" physicalPath="%IIS_SITES_HOME%\WebSite1" />
</application>
5.2 A kontroller és környezete - Az alkalmazás kiindulási pontja. A global.asax 1-73
<bindings>
<binding protocol="http" bindingInformation=":8080:localhost" />
</bindings>
</site>
<site name="FirstMVCApp" id="2">
<application path="/" applicationPool="Clr4IntegratedAppPool">
<virtualDirectory path="/" physicalPath="D:\........\FirstMVCApp" />
</application>
<bindings>
<binding protocol="http" bindingInformation="*:2927:localhost" />
</bindings>
</site>
<site name="MvcApplication1" id="3">
<application path="/" applicationPool="Clr4IntegratedAppPool">
<virtualDirectory path="/" physicalPath="D:\.........\MvcApplication1" />
</application>
<bindings>
<binding protocol="http" bindingInformation="*:18005:localhost" />
<binding protocol="http" bindingInformation="*:18005:192.168.1.100" />
</bindings>
</site>
Előfordul, hogy valahol a neten egy cikket olvasunk, amiben leírják, hogy ezt és ezt kell beállítani a
web.config-ban. Sokszor kimarad, hogy mégis hova, melyik szekcióba kéne tenni azt a néhány
emlegetett beállítást. Ezért is jó tudni ezekről az "ős" beállító fájlokról. Ha az alkalmazásunk web.config
fájlját szeretnénk bővíteni valamilyen beállítással, akkor a machine.config melletti web.config-ot
elővéve nagy esélyünk van rá, hogy azonosítani tudjuk a keresett szakaszt. A másik nagyon jó érv, hogy
megjegyezzük ezeket az, hogy a normál web.config és machine.config fájlokból van egy-egy .comments
kiterjesztésű fájlváltozat is. Ezek belsejében fel vannak sorolva a beállítások mellett a lehetséges
további paraméterek és értékkészletük, típusuk sok-sok komment sorban. Az önleíró változatok. Ezek
mellett még vannak előkészített beállítássablonok is különböző biztonsági szintekhez. Hmm, hol is
láttam ilyeneket régen? Ja, igen: httpd.conf, my.ini, php.ini.
Mi történik az után, hogy az újdonsült mini alkalmazásunk elindult? Mint minden rendes ASP.NET alapú
alkalmazásnál a vizsgálódást a projektünk gyökerében található Global.asax fájllal kell kezdeni. Ennek
a fájlnak pontosan az a szerepe, mint az ASP.NET Web Forms alkalmazásoknál, egy HttpApplication
leszármazott példányt határoz meg, ami nem más, mint a mi alkalmazásunk. Ebben lehetőségünk van
alkalmazás szintű eseménykezelők írására. Ilyen eseménykezelők az első request megérkezésétől az
alkalmazásunk leállásáig különböző lehetőségeket adnak, hogy olyan kezelőket írjunk, amik az adott
helyzetben megváltoztathatják vagy kiegészítik az alapértelmezett viselkedést, esetleg környezeti
értékeket állítanak be. Ezek ugyan eseménykezelők, de nem úgy, mint a Windows Forms-ban
használatos eventek. Nem kell sehova sem feliratkozni, hogy lefussanak. A metódusok szabványos
elnevezésük alapján kerülnek meghívásra. Ezek az eseménykezelők opcionálisak. Viszont ahhoz, hogy
5.2 A kontroller és környezete - Az alkalmazás kiindulási pontja. A global.asax 1-74
Szinte minden ASP.NET alapú webalkalmazásnál előfordul, hogy valamit még az előtt szeretnénk
csinálni, hogy a beérkezett request megérkezne az oldal normál feldolgozásához, például az actionhöz.
Ezért egy sereg olyan eseménykezelő metódus áll rendelkezésre, ami az oldalgenerálás teljes
lefutásának a lépései előtt és az adott lépés után lefutnak. A felsorolás sorrendje egyben a lépések
sorrendje is. A legfontosabbakat csillaggal is megjelöltem.
Application_AuthenticateRequest()* – A felhasználó hitelesítése előtt fut le. Itt lehet még egyedi
felhasználói hitelesítést készíteni.
Ezen kívül még ott vannak azok az eseménykezelők, amik nem minden request esetén indulnak el,
hanem az alkalmazás életciklusához kötődnek.
Session_Start()* – Új Session objektum létrejötte után. Ez minden új látogató esetén lefut, akinek nincs
session azonosítója és az alkalmazás számára szükséges lesz.
Egy fontos kiegészítést meg kell említeni, ami a webszerver erőforrás kezelési mechanizmusából
következik: azt, hogy az alkalmazásunk nem csak egyszer tud elindulni és nem áll le a request
kiszolgálása után azonnal. Az alkalmazás elindítása, a dll-ek betöltése, a köztes kódok fordítása
időigényes ezért ezzel gazdálkodni kell. Az IIS webszervereken az alkalmazásunk un. Application pool-
ban fut. Egy AppPool közös lehet több alkalmazás számára is (nagyobb alkalmazásoknál ez nem
ajánlott). Egy ilyen AppPoolnak több beállítási lehetősége közül az egyik az, hogy mennyi tétlenségi idő
után legyen újrahasznosítva (más szóval alapállapotra hozva), aminek eredményeképpen az összes
felügyeletére bízott alkalmazásnak is vége lesz. Ez alapértelmezetten 20 perc. Ha nincs feladata egy
AppPoolnak, azaz egyik alkalmazásához sem jön egy request sem a beállított időhatáron belül, akkor
újrahasznosításra kerül (memória felszabadítás) és visszaáll a kezdeti állapotára. A másik
megjegyzendő, hogy a Visual Studio-hoz mellékelt beépített fejlesztői webszerverek is így működnek.
A leállást követő újabb request hatására az alkalmazás úgy indul el, mintha még sosem futott volna.
Újrafeldolgozásra kerülnek az Application_Start-ban megfogalmazott beállítások.
WebApiConfig.Register(GlobalConfiguration.Configuration);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
AuthConfig.RegisterAuth();
}
}
A fenti kódban csak olyan további metódusok (Register…) kerülnek meghívásra, amelyek az MVC
beállításáért felelnek. Hogy ezek mit regisztrálnak, azt a későbbi fejezetek részletesebben le fogják írni.
Tegyünk egy tanulságos kísérletet. Állítsuk le a fejlesztői webszervert a system tray-en (a képernyő
jobb alsó része, általában).
Helyezzünk el egy breakpointot (Leállított programnál a megállítandó soron nyomjuk meg az F9-et) az
Application_Start-ba majd indítsuk el az alkalmazásunkat. Elsőre meg fog állni a program futása, mikor
a böngészőtől megérkezik a kérés. Engedjük hadd fusson tovább (F5). Ezután frissítsük az oldalt, vagy
lépkedjünk az alkalmazásunk menüjében (Home, Contact) és nem fog megállni többé. Most állítsuk le
a program futását (pl. a Shift+F5-el), majd indítsuk el újra. Nem fog megállni most sem az
Application_Start-ban (ezt tetszőleges számban ismételgethetjük, de egy idő után unalmassá fog
válni). Állítsuk le megint az alkalmazást és menjünk el a FilterConfig.RegisterGlobalFilters metódusába
és írjunk bele valami hatástalant, pl. int i=1; . Ezután indítsuk el újra az alkalmazást, és ha érthetően
sikerült leírnom, akkor megint meg fog állni a breakpointnál. Hasonló eredményre jutottunk volna, ha
nem a metódusba írunk kódot, hanem az alkalmazás alapmappájában levő web.config fájlba írtunk
volna akár csak egy ártalmatlan soremelést is (ráadásul ilyenkor még az alkalmazásunkat sem kell
leállítani a VS-ban). A fenti kísérlet elvégzése (ha még soha nem csináltunk ilyen) és az eredményének
megszívlelése, számos későbbi kellemetlen meglepetéstől fog minket megkímélni a valós fejlesztés
5.2 A kontroller és környezete - Az alkalmazás kiindulási pontja. A global.asax 1-76
5.3. Routing
Már volt szó arról, hogy a request egy kontroller egy metódusát célozza meg az URL alapján (és nem
egy fájlt, mint az a webszerver normál viselkedése lenne). Most arról lesz szó, hogy hogyan működik
ez a mechanizmus. Menjünk vissza a kályhához, és vegyük megint elő a global.asax-ot. Nézzük meg a
route konfigurációt: RouteConfig.RegisterRoutes(RouteTable.Routes); Ami semmit sem mond, tehát
menjünk tovább az App_Start mappában levő RouteConfig.cs –hez, mert itt van az implementáció
lényegi része.
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
}
9. példakód
Ez egy statikus metódus, tehát a hasznos kód lehetne akár az Application_Start-ban is. Az MVC előző
verzióiban (1-2-3) ugyanis ott is volt. Ezért is volt az előző körutazás, hogy meg tudjam mutatni, hogy
ez a kódcsoportosítás az MVC 4 egy új színfoltja. Ugyanígy az App_Start mappában találhatjuk meg a
FilterConfig, AuthConfig, stb. osztályokat, egyszóval az alkalmazás induláskori paraméterezéseit.
A metódus bemeneti paramétere egy gyűjtemény. Ehhez a gyűjteményhez tudunk hozzáfűzni újabb
route bejegyzéseket. A gyűjteménybe kerülő bejegyzések sorrendje fontos, a sorban elől levőket előbb
is értékeli ki az MVC. Ha a sorban egy elemet megfelelőnek talál, akkor a továbbiakkal nem foglalkozik.
Az első illeszkedő lesz a győztes.
Volt már szó arról, hogy az URL domain név utáni szakasza alapján határozódik meg a kontroller és az
action. Ismétlésként a /Home/Contact a HomeController.Contact() metódusát jelenti pont a fenti
definíció miatt. Kicsit továbblépve lehetséges az is, hogy a Contact metódusnak rögtön egy paramétert
átadjak, ha ezt írom /Home/Contact/1 és ha a Contact metódus szignatúrája 22 ilyen:
az MVC a /Home/Contact/1 URL végén levő 1-et az ’id’ paraméterben átadja a metódusnak.
Nézzük akkor a 9. példakódot. A route definíciónak van egy neve, „default”, ami most nem lényeges,
de attól hogy ’default’, még nem lesz alapértelmezett. Ez csak a neve, lehetne ’akármi’ is. Ha több route
bejegyzésünk van, akkor a speciálisak előre az általánosabbak hátra kerüljenek a sorban. Így a ’default’
értelmű a legutolsó legyen. A definíciónak van egy URL mintája „{controller}/{action}/{id}”, amit úgy
kell értelmezni, hogy az URL-t a ’/’ jelek mentén szakaszokra bontjuk és a szakaszok egymás után
controller-t, action-t és id-t jelentenek. Ha az URL ráillik erre a mintára, akkor az MVC számára világos
lesz, hogy ezt a route mintát kell használnia ahhoz, hogy megtalálja a kontrollert és annak az action
metódusát, és találjon az action metódushoz id paramétert. A '/' jel nem kötelező érvényű, de ez
tekinthető általánosnak az "URL, mint bejárási út + erőforrásnév" séma alapján. Lehetne használni akár
22
a metódusnév, a paraméter lista típusosan értelmezve és a visszatérési típus és együtt
5.3 A kontroller és környezete - Routing 1-78
kötőjelet is, sőt vegyesen is. A '/' jelre itt inkább úgy érdemes gondolni, mint az URL minta statikus
szakaszára, ami nem vesz részt a kontroller, action, paraméter kiválasztásában.
A MapRoute-nak van még egy "defaults" paramétere is. Ebben azt tudjuk meghatározni, hogyha az
URL nem teljes, de az eleje egyébként ráillene a mintára, akkor mit helyettesítsen be a hiányzó
szakaszba. Emiatt van az, hogyha elindítjuk az alkalmazást, akkor a böngészőben egy ilyesmi URL-t
láthatunk: http://localhost:9999, de ugyanez az oldal fog megjelenni, ha a http://localhost:9999/Home
vagy ha a http://localhost:9999/Home/Index URL-t írjuk. A 'Home' mint az alapértelmezett
kontrollernév és az 'Index', mint az alapértelmezett action név.
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(name:"Kategoriak",
url: "{controller}/{action}/{category}/{id}",
defaults: new { controller = "Home", action = "Index", category = UrlParameter.Optional,
id = UrlParameter.Optional });
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional });
}
Ezzel azt tudjuk elérni, hogy a friendly URL továbbra is jól olvasható maradjon, és ne kelljen '?',' =' és
'&' jeleket bevezetni az URL-be, viszont az id mellett még a "category", mint metódus paraméter kapjon
értéket az URL-ből. Ehhez természetesen az index metódus paraméter listáját meg kell változtatni
ilyesformára:
<hgroup class="title">
<h1>@ViewBag.Title.</h1>
<h2>@ViewBag.Message</h2>
</hgroup>
A példa azonban sántít, ugyanis a „default” route bejegyzés soha nem fog érvényre jutni, hisz az általa
definiált URL mintát elfedi az újonnan definiált route bejegyzésünk. Ahhoz, hogy tényleg elkülönüljön,
célszerű átírni úgy, hogy egyedi legyen a felvezető szó az URL elején.
url: "Webshop/{controller}/{action}/{category}/{id}".
Ettől függetlenül működni fog az eredeti route definíció, amihez még mindig egy ilyen URL illeszkedik:
/Home/Index/100.
Ez egy olyan szituáció volt, ami rávilágít arra, hogy a route bejegyzések készítésénél észnél kell lenni,
mert nagyon könnyen készíthetünk értelmetlen vagy a többi bejegyzést értelmetlenné tevő új route
mappingeket.
URL Eredmény
/webshop/Home/Index/Vilagitas/16 Kategória: Vilagitas, Id: 100
/Home/Index/100 Kategória: null, Id: 100
A nagyszerű az egészben az, hogy a route definícióban megnevezett paraméterek (id, category)
pontosan leképződnek az action metódus paramétereire, név szerint. Ezért van az, hogy amíg a
webshopos route-nál a kategória metódusparaméter ki van töltve, addig az eredetinél nincs csak az id,
mert annak a route definíciójában is csak az ’id’ szerepel. Valójában ez a példa sem életszerű, mert
milyen célt akarunk elérni azzal, hogy a HomeController.Index() metódusának belső kódja, két olyan
route beállítást is kiszolgáljon, amik nyilvánvalóan valamilyen elkülönült üzleti igényt akarnak
kielégíteni (pl. nyitólapot és egy webshopot). Ha azonban az új route default értékeinél a controller
tulajdonságot átírjuk, mondjuk ’Termekek’-re
{controller}/{action}/{id}
Home /Index /Butorok/101
Az URL szakaszos értelmezése mellett még mindig meg van a lehetőségünk, hogy query stringgel
egészítsük ki az URL-t. A webshopos URL path-t így felírva:
5.3 A kontroller és környezete - Routing 1-80
Az MVC, ha nem találja az URL path-ban a metódus paramétert név szerint, akkor még a query string-
ben is megnézi, hátha ott van.
Route konkrétabban
Az előbbi példákban több olyan fura helyzet is előjött, amit az egyértelműség hiánya okozott. Például,
hogy a {category} URL szakasz, action vagy paraméter. Nem is olyan egyszerű eldönteni. Célszerű a
route bejegyzésekkel csínján bánni, mert az átfedések miatt nem várt helyzetek is előfordulhatnak. Ha
ránézünk erre az URL-re: /Home/Index/100/kendermag és az előbbi/alábbi route definícióra, akkor
feltehetjük a kérdést, hogy ezzel mi lesz?
routes.MapRoute(name:"Kategoriak",
url: "{controller}/{action}/{category}/{id}",
defaults: new { controller = "Home", action = "Index", category = UrlParameter.Optional,
id = UrlParameter.Optional });
A megfeleltetés az lesz, hogy "category" = 100 és "id"="kendermag". Így még eljut az actionig és
paraméterben át is adódnak az értékek egy ilyen csupa stringes szignatúra esetén:
De ennél már nem várt eredményt kapunk, mivel az Id ritkán string alapú, és a category-nál sem szám
az elvárható:
Egyébként is, ne engedjünk be akármilyen tartalmú URL-t, szűrjük meg mielőtt gondot okozna!
A vázolt problémákon a route megszorítások, korlátozások (route constraints) szoktak segíteni. A
korlátozást elsődlegesen regular expression-el lehet deklarálni, hasonlóan a 'defaults:' route
értékekhez, egy anonymous osztálydefinícióval. Fel kell sorolni azokat a paramétereket, amelyek
tartalmának vizsgálatára korlátozást kívánunk bevezetni:
routes.MapRoute(name: "Kategoriak",
url: "{controller}/{action}/{category}/{id}",
defaults: new {controller = "Home", action = "Index",category = UrlParameter.Optional,
id = UrlParameter.Optional},
constraints: new { id = @"^\d+$", category = @"(Butorok|Textil|Vilagitas)" }
);
A fenti példa szerint a route definíció csak akkor érvényes, és csak akkor kell számításba vennie a Route
rendszernek, ha az "id" szakasz legalább egy karakterből álló szám és a "category" helyén levő URL
szakasz tartalma a | jellel elválasztott szavak egyike.
Bármilyen értelmes kifejezést megadhatunk, de csak az adott nevű route szakaszra. Több paramétert
egyszerre érintő megszorítást így nem tudunk meghatározni. De hogy ilyen esetben se kelljen sokat
ügyeskedni, lehetőség van definiálni egy IRouteConstraint interfészt megvalósító osztályt, amiben
úgy vizsgáljuk a bejövő URL szakaszokat, ahogy csak akarjuk.
5.3 A kontroller és környezete - Routing 1-81
object categobject;
if (!valuesDict.TryGetValue("category", out categobject) || categobject == null)
return false;
switch (categobject.ToString())
{
case "Butorok":
return id < 100;
case "Textil":
return id < 10;
case "Vilagitas":
return id < 5000;
default:
return false;
}
}
}
A fenti kód megvizsgálja, hogy a különböző kategóriák szerint az id értéke a megadott határ alatt van-
e.
Azok a bizonyos route/URL szakaszok, a "valuesDict" paraméterben érkeznek meg név-érték párokban.
A fenti kód is ebből a szótárból próbálja meg kiszedni a route szakaszokat név szerint és megvizsgálni
a képzeletbeli üzleti szempontok szerint. A Match metódusnak true-t kell visszaadnia, ha a route
bejegyzés szerinte illeszkedik. Az illeszkedést vizsgálhatjuk a RouteValueDictionary alapján (ezt teszi a
fenti kód is), de akár a bejövő requestet is megvizsgálhatjuk, ami httpContext-ben elérhető Request
objektumban van tárolva. Ez utóbbival lehetséges route bejegyzéseket elkülöníteni protokoll szinten
is, például HTTP vagy HTTPS alapon.
routes.MapRoute(name: "Kategoriak",
url: "{controller}/{action}/{category}/{id}",
defaults: new { controller = "Home", action = "Index", category = UrlParameter.Optional,
id = UrlParameter.Optional },
constraints: new { id_akarmi = new MultiConstraint()},
);
Azt is le lehet fixálni, hogy a route definíció csak akkor legyen érvényes, ha a megcélzott kontroller az
adott névtérben van. Erre a "namespaces" paraméter szolgál, ahol akár egyszerre több névteret is
megadhatunk, mivel egy string[] tömböt vár. Ez megkötés és együttműködik a route contraints-al, de
használható magában is.
routes.MapRoute(name: "Kategoriak",
url: "{controller}/{action}/{category}/{id}",
defaults: new { controller = "Home", action = "Index", category = UrlParameter.Optional,
id = UrlParameter.Optional },
namespaces: new string[] { "MvcApplication1.Controllers" }
);
5.3 A kontroller és környezete - Routing 1-82
A namespaces megszorítás, akkor fog hasznunkra válni, ha az alkalmazásunk modulárisan épül fel,
amikor a kontroller nem szükségszerűen a fő projektben van definiálva, hanem egy külső dll-ben.
Bevallom őszintén a fejlesztés route definiálási szakaszában számos meglepetésben volt már részem,
ezért is kerülöm a sok bejegyzést. Célszerűnek tartom, hogy a lehető legkevesebb definíciót készítsük
el. Azonban ha erre nincs mód és több route mappelést kell meghatározni, akkor a route bejegyzéseket
már a kezdeteknél lássuk el megszorításokkal. Ilyen helyzetben már érdemes tesztelni is a route
bejegyzéseket, erre több eszköz is létezik. Személyesen a Glimpse 23 zseniális NuGet csomagját
ajánlom. Ebben külön "Routes" nevű fül áll rendelkezésre az URL alapján kiértékelődött, aktív-inaktív
route bejegyzések tesztelésére. Az alábbi képen a http://localhost:18005/Home/Index/100 -re adott
elemzést láthatjuk:
A nagy zöld sáv jelenti, hogy a "Default" route határozta meg a route szabályt. A piros nyíllal jelzett sor
mutatja, hogy az előbb definiált MultiConstraint osztály kiértékelési eredménye: false. A Glimpse csak
akkor működik, ha a bejövő requestnek volt normál eredménye, így ha az URL és a route alapján nincs
elérhető kontroller és action, akkor ez sem fog segíteni. Érdemes a többi képességét is
áttanulmányozni, mert sok esetben jobb megoldást nyújt, mint egy sziszifuszi debuggolás.
Végezetül az említett ajánlás még egyszer: A rendszer működése miatt ajánlatos a modellosztályunkon
a route mintában szereplő szakaszokkal azonos nevű propertyk mellőzése. Így nem tanácsos
'controller', 'action', és az előbbi példát alkalmazva a 'category' property neveket definiálni, függetlenül
a kis és nagybetűs változatoktól, egy ilyen route szakaszdefiníció esetén:
{controller}/{action}/{category}/{id}",
Ennek okára majd a beérkező request feldolgozásánál még visszatérünk. Addig sem kell megijedni,
lehet használni ezeket a neveket is csak egyes ritka esetekben nem várt eredményt kaphatunk.
A route bejegyzések célja, hogy az MVC infrastruktúrája meg tudja határozni, hogy melyik kontroller
melyik actionjét kell használnia. Néha azonban ez nem elég.
23
http://getglimpse.com/
5.3 A kontroller és környezete - Routing 1-83
Előfordul, hogy hibrid MVC + Web Forms alkalmazást fejlesztünk, amiben el szeretnénk rejteni az URL-
ből, hogy (még mindig ) vannak .aspx fájlok is. Ez egy Web Forms -> MVC migrációnál könnyen
előfordulhat. Ilyen esetben jól jöhet a MapPageRoute extension metódus.
Tegyük fel, azt szeretném elérni, hogy az AspPages/One.aspx fájlok és társai a /forms/ URL alól
legyenek elérhetőek. Szemléltetésképpen ebben a táblázatban írtam néhány példát:
Látható, hogy a második paraméter {webform} mintája által lefedett szakasz tartalma átmásolódik a
harmadik paraméter azonos nevű mintájának a helyére. Szintén használhatóak a route megszorítások
és a default értékek úgy, mint a MapRoute-nál.
AttributeRouting
Az MVC alkalmazásunkban vélhetően lesznek olyan közös funkcionalitású actionök, amelyek logikailag
nem kötődnek csak egy kontrollerhez. Leginkább az oldal egy részletének az előállításáért felelnek
(child action). Ilyen szokott lenni például a közösen használt fájlfeltöltés, letöltés,
hibakezelés/megjelenítés, dinamikus fejléc, menü és lábléc kezelése. Az ilyenek számára rendszerint
egy "CommonController"-t lehet biztosítani. Ekkor az URL rendszerint így néz ki:
/common/headermenu/1. Ezzel nem is szokott gond lenni. Viszont ez az egysíkú megközelítés már nem
lesz annyira tagolt, ha történetesen a főmenü elemeit oldalspecifikus kiegészítő menüelemekkel, vagy
toolbar jellegű gombokkal szeretnénk oldalról oldalra dinamikusan bővíteni. Ekkor a menüelemek egy
részének az összeállítása az aktuális és nem a common kontroller feladata lesz. Valahogy a kettőnek
együtt kéne működnie, emiatt ez nem egy jó felépítés. Talán egy még egyszerűbb eset vagy probléma,
amikor az oldalon helyi/popup menüt szeretnénk csinálni. Ez már tényleg csak az aktuális kontrollerhez
kötődik. Ha ezek után úgy gondoljuk, hogy az alkalmazás URL-jeinek a struktúrája jól tagolt legyen,
akkor kis nehézségbe fogunk ütközni.
Miért érdemes máshogy is tagolni az URL-eket, ha már tagoltak a kontroller/action minta alapján?
Például az alkalmazás átstrukturálhatósága és karbantarthatósága miatt. Ezt minimálisan
névkonvenciókkal és szabványosított mintákkal tudjuk biztosítani. Tegyük fel, hogy a helyi menük
kezelését egyedileg, egyelőre kontroller szinten oldjuk meg. Viszont nyitva szeretnénk hagyni a
lehetőséget arra, hogyha úgy ítéljük meg az egész popup menükezelést mégis egy (pl.: commonpopup)
kontrollerre akarjuk bízni. Ekkor nagyon jól járunk, ha a helyi menük kezelésére már a kezdeteknél
funkcionális URL/route-mintát alkalmazunk. Például:
Később lehet készíteni egy popupmenu kontrollert. Esetleg, ami még jobb egy WebAPI kontrollert,
amivel egy javascript alapú helyimenü-kezelést tudunk kiszolgálni.
Teljesen más szempont lehet, ha a megrendelőnek az az igénye, hogy a régi rendszerét cseréljük le,
egy korszerű, MVC alapú alkalmazásra, de úgy, hogy a funkciók (egy része) azonos URL-el legyenek
elérhetőek. (Például hivatkozások vannak rá dokumentumokban/weboldalakon, más automatikus
rendszerek hívogatják, stb.). Ekkor valószínűleg nem lesz elégséges az MVC beépített routes.MapRoute
extension metódus által szolgáltatott lehetőség.
Tudnám még tovább ragozni a modularizált MVC alkalmazásfejlesztés esetével is, de a lényeg, ha
szükségünk lenne arra, hogy egy kontrolleren belül az actionök különböző URL/route mintára
reagáljanak, akkor a route definíciókat action szinten kell biztosítani. Ezt a normál route mapping
módszerrel rendkívül körülményes jól megoldani. Ráadásul elveszik a logikai kapcsolat a route map
bejegyzés és a kontroller actionök között. Ilyenkor elég furcsa azt csinálni, hogy miden egyes MapRoute
mellé kommentbe odaírjuk, hogy "//ez a XY kontroller YZ actionjéhez szükséges".
Az MVC 4-ben már elérhető HttpGet, HttpPost, HttpDelete és a többi HTTP method alapú attribútumok
rendelkeznek egy új konstruktorverzióval, amin keresztül route mintát lehet meghatározni az
actionhöz. Ezek mellett megjelent egy HttpRouteAttribute is, amivel szintén route mintát lehet
rendelni az actionhöz, de úgy hogy nem kötjük ki a HTTP methodot.
HttpRouteAttribute(string routeTemplate)
A routeTemplate legegyszerűbb alakja, amikor csak egy alternatív URL path-t adunk meg:
[HttpRouteAttribute("RC/Name1")]
[HttpGet("RC/Name1")]
public ActionResult Details1()
{
return View();
}
Az URL path pontosan az lesz, amit megadtunk: RC/Name1. A fenti kód hibát fog okozni, mert nem
lehet két azonos route a rendszerben. A két attribútum közül csak egyet lehet egyszerre használni. A
route paraméter nem határozza meg a View fájl nevét, nem úgy, mint az ActionName attribútum
paramétere. Az új route útvonal tényleg alternatív, mert az eredeti kontroller/action alapú route addig
megmarad, amíg ki nem töröljük. Ha ez egy nyilvános site lenne, akkor erre figyeljünk, mert a Google
keresőmotorja lepontozza az azonos site-on több URL-el elérhető tartalmakat 24 . Az AcceptVerbs
attribútum számára a RouteTemplate tulajdonsággal adható meg a route útvonal.
A következő példa egyben előírja, hogy a megadott útvonalon az action post HTTP methoddal hívható:
[HttpPost("RC/Name1")]
public ActionResult Details2() {
return View();
}
24
Vagy a robots.txt-ben zárjuk ki.
5.3 A kontroller és környezete - Routing 1-85
Viszont az előző Details1 metódussal együtt nem használható, mert azonos route útvonalat jelent.
Kettő egyforma még akkor sem lehet, ha más Http* attribútumban definiáltuk. Arra viszont van
lehetőség, hogy egy action számára több eltérő route definíciót is megadjunk.
[HttpGet("RC/Name2")]
[HttpGet("RCDemo/Name2")]
public ActionResult Details3()
{
return View();
}
Lehetőség van paraméteres route mintát is meghatározni. Ráadásul úgy is, hogy a route minta
metódusparamétert jelentő szakaszára típusmegkötést is adhatunk. Jelen esetben az 'id' URL szakasz
helyén csak egész szám állhat, és kötelező hogy ott egy szám legyen.
[HttpGet("categories/Details/{id:int}")]
public ActionResult Details4(int id)
{
return View();
}
[HttpGet("categories/{categ}/Details/{id:int}/{defaulvalue=Alapertek}/{notanoption}",
RouteName = "Details5Route")]
public ActionResult Details5(string categ, int id, string defaulvalue, string notanoption)
{
return View();
}
@Html.RouteLink("Details4","categories/{categ}/Details/{id:int}/{defaulvalue=Alapertek}/{notanoption}",
new {id=111}) //Hivatkozás route mintára, mint névre
@Html.RouteLink("Details6 opc.","Details6Route", new {categ="Cipők", id=111}) //Hivatkozás route névre
A RouteLink-ről még lesz szó. <a> tagot generál a megadott route URL-re.
Az utolsó példában a definíció végén levő {name?} opcionális route szakasz. Ezt jelenti a kérdőjel.
[HttpGet("categories/{categ}/Details/{id:int}/{defaulvalue=Alapertek}/{name?}")]
public ActionResult Details6(string categ, int id, string defaulvalue, string name)
{
return View("Details5");
}
5.3 A kontroller és környezete - Routing 1-86
Az előző route definíciókban láttuk ezt a mintát: {id:int}. Ezzel meghatároztuk, hogy az id helyén csak
egész szám lehet. Léteznek még további megkötési formulák is, és akár újakat is hozhatunk létre.
A 'categ' helyén érkező URL szakasznak minimálisan 10 karakter hosszúnak kell lennie.
Itt elvileg elég lett volna a min(10) használata, mert az azt is megköti, hogy szám legyen. Akkor lenne
most létjogosultsága, ha a lebegőpontos számokat ki akarjuk szűrni.
Az MVC 5 jelenlegi verziójában (2013.07.13) ezt a megkötési lehetőséget csak az attribútum alapú
route meghatározásban lehet használni. A normál RouteMap–el bejegyzett esetekben nem.
5.3 A kontroller és környezete - Routing 1-87
Route prefixek
A hagyományos kontroller/action alapú route-olásban az a jó, hogy az actionök a kontroller neve által
meghatározott URL előtagtól kiindulva érhetőek el. Az URL végén csak az action neve változik. Itt is van
lehetőség, hogy az egyedi attribútum alapú route beállításokat prefixszel láthassuk el:
[RoutePrefixAttribute("RouteDemo")]
public class RoutingAttrController : Controller {}
Ilyenkor minden egyedileg, action szinten meghatározott route definíció elé bekerül a RoutePrefix
szöveges paramétere. Így az eddig használt route útvonalak így fognak kiegészülni:
"RouteDemo/RC/Name1", "RouteDemo/RC/Name2"
Érdekes lehetőség, hogy meg lehet csinálni azt, hogy egy normál alapprojektbeli kontrollert ellátva a
RouteAreaAttribute úgy viselkedjen, mintha az adott nevű areaban25 lenne. Legalábbis route szinten.
RouteAreaAttribute(string areaName)
A hagyományos route definícióknál kell beállítani, hogy ez az attribútum alapú route meghatározás
működjön.
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
}
A vastagon kiemelt rész első sora meghatározza, hogy melyik kontrollerek esetében induljon el a route
attribútumok kiértékelése. Így ebben a példában csak a RoutingAttr kontroller esetén lesz hatásos. A
második sora az extension metódusával hozzáadja az attribútumokból kinyert route definíciókat a
normál route listához. Nem kötelező megadni a kontrollerek felsorolását, és akkor az összes kontrollert
feltérképezi, de szerintem ezt nem tanácsos így használni.
Ez a képesség a VS2013 preview alatt még nem érhető el. Azonban nem muszáj kivárni, míg az MVC 5
megérkezik. Mivel pontosan ugyan erre a célra és szinte azonos paraméterezéssel, már rendelkezésre
áll az Attribute Routing nevű NuGet csomag. Hivatalos oldala: http://attributerouting.net.
25
A 11.2 fejezetben lesz róla szó.
5.4 A kontroller és környezete - Controller 1-88
5.4. Controller
Az általunk megírt kontrollernek nincs kötelezően előírt konstruktora, de írhatunk is, és ebben az egész
kontrollerre jellemző környezetet be tudjuk állítani még a konkrét action metódus meghívása előtt.
public ActionDemoController()
{
//Környezeti beállítások. Adatbázis/WCF kapcsolat.
}
Intermezzo:
Fontos, hogy ha nincs valami nagyon nyomós okunk rá, ne használjuk a kontrollerben (és máshol sem)
statikus adattagokat vagy tulajdonságokat. Elsőre nagyon kényelmesnek tűnhet, hogy ide
mentegetünk adatokat a kérések kiszolgálása között, később azonban kezelhetetlen kódot és/vagy
nem kívánt mellékhatásokat okozhat. Mielőtt elkezdjük egy adat statikus definíciójának írását,
kérdezzük meg magunkat, hogy biztos, hogy nincs egy jobb OOP-s megoldás vagy egy (elfeledett)
tervezési minta? A statikus tárolás egyszerűen nem illik ehhez a webes világhoz, ahol minden olyan
állapotmentes. Amit statikusan deklarálunk az közös lesz az összes felhasználó összes állapotában,
amíg az alkalmazásunk fut. Sok esetben egy statikus adattárolás nem más, mint téves illúzió, amiről
akkor hull le lepel, amikor az alkalmazás kikerül a mi kis egyfelhasználós fejlesztési környezetünkből és
elkezdik többen használni egyszerre.
Az után, hogy az MVC példányosította a kontrollert, nagyon sok hasznos adatot ad át propertyken
keresztül, amiket az action metódusban fel tudunk használni. Nézzük meg az egyik legfontosabbat,
amin keresztül felfedezhetjük az ASP.NET MVC kontextusoknál gyakran alkalmazott property kiemelést
is.
ControllerContext
Ebben van tárolva a kontroller működése számára egy teljes információs blokk. Érdemes egy kicsit
megnézni a tartalmát. Ugyanis a Controller ősosztályon elérhető HttpContext, Request, Profile,
Response, Server, Session, User, RouteData propertyk csak ennek a ControllerContext belső
propertyjeinek a kivezetései (csak hogy ne kelljen az objektumhierarchiában keresgélnünk). A
Controller ősön levő property nevek és a hierarchia belső property nevei azonosak.
ControllerContext
o HttpContext
Application
Request
Profile
Response
Server
Session
User
o RouteData
Emiatt az action kódjába írva, az alábbiak mindkét esetben a Session objektumot adják vissza:
- var session = this.ControllerContext.HttpContext.Session;
- var session = this.Session;
Röviden ez volt tehát a ControllerContext, ami igen jól példázza azt a megközelítést, hogy az oldal
feldolgozásának az életciklusában sok olyan objektum példány propertyje érhető el, amit más módon
is meg tudunk találni, más objektumon más propertyn is ki van vezetve. A továbbiakban ezeket a
mélyről kivezetett propertyk célját nézzük meg.
5.4 A kontroller és környezete - Controller 1-90
Itt most csak azokról lesz említésszerűen szó, amik elég fontosak és elég gyakran kerülnek
felhasználásra a kontroller kódjában. Egyébként egy külön könyvet is lehetne szentelni annak, ha egy
minden propertyt lefedő leírást szeretnénk készíteni. Ezért legyen ez csak egy amolyan áttekintő
felsorolás, mivel a legtöbbel még találkozni fogunk ott, ahol a téma érinti a felhasználásukat.
HttpContext – Ez a gyökere minden olyan adatnak, ami az oldal feldolgozása során az adott pontig
elérhetővé vált. A kontroller esetében ez egy jól feltöltött objektumot jelent. A lényeges elemei:
Profile – A bejelentkezett felhasználóhoz köthető egyedi adatok tárolója. A használata egy un.
profile providert igényel, amit a web.config-ban tudunk beállítani. Ebben a könyvben nem lesz róla szó,
mivel az MVC (4-től) egy sokkal rugalmasabb felhasználói profilkezelést vezetett be.
RouteData – Ennek a Values dictionary-jében találhatók azok a kulcs-érték párok, amik a futó
kontroller és action kiválasztásánál szerepet játszottak. A Home/Index által elért Home kontroller Index
actionjében nézve ilyen értékeket találunk benne: ”controller” – ”Home”, ”action” – ”Index”. Emellett
szintén ez tárolja az URL paramétereket is.
Binders – A rendelkezésre álló model binder-ek. Később igen részletesen fogunk velük foglalkozni. Ezek
felelősek a requesttel érkező adatok típusos formára hozásáért.
ModelState – A beérkező request alapján az MVC képes létrehozni a modell példányunkat és amint
láttuk a modell tulajdonságai és a komplett modell is validálható. A validáció eredménye kerül ebbe a
ModelState tárolóba.
Session
A Session az a tároló, ahova olyan adatokat tehetünk, amelyekre szükség van az azonos felhasználótól
érkező egymás utáni kérések során. Ide lehet tenni pl. a webshop bevásárló kosár tartalmát, vagy bármi
olyat, amit a felhasználó egy előző oldalunkon már beállított és nem szeretnénk elveszíteni. A HTTP
protokoll állapotmentes, a Session tudja biztosítani, hogy mégis legyen egy helyünk, ahova a
felhasználóhoz tartozó kosár tartalmát menteni tudjuk. Amikor a felhasználó a böngészőjével először
meglátogatja az oldalunkat és tárolni szeretnénk valamilyen adatát a Session-be, akkor az ASP.NET nyit
számára egy új session-t és ezt azonosító számmal ellátja, az azonosító számot pedig egy
”ASP.NET_SessionId” nevű cookie-ba helyezi, amit így megkap a böngésző. (Hogy valóban oda helyezi,
vagy máshogy oldja meg, az beállítás és némi automatizmus kérdése). Amikor a felhasználó tovább
böngészik , akkor a böngészője indítja a következő requestet, de már úgy, hogy ebbe belecsomagolja
az előbb kapott cookie-t, ilyenkor az ASP.NET a cookie-ban levő azonosító alapján megkeresi az
előzőleg létrehozott session objektumot. Mire a kontrollerhez megérkezik a kérés ez a session
objektum ott lesz ebben a Session propertyben, nekünk csak használni kell.
A Session egy nem típusos szótárat rejt. Az elemeire tudunk hivatkozni az indexük és a nevük alapján.
Ilyen egyszerűen:
Session["HomeItem"] = 10;
return View();
}
10. példakód
5.4 A kontroller és környezete - Controller 1-92
Az Index metódusban a „HomeItem” elemébe (nem baj, hogy még nincs is ilyen eleme, majd lesz)
belerak 10-et. Ha a böngészőben az About menüre kattintunk, akkor a hozzá tartozó About
metódusban vissza tudjuk kapni a 10-est.
A példa legalább egy sebből vérzik. Mi van a felhasználó egyből az About oldallal nyit? A kód el fog
szállni, mivel a session kollekcióban nem lesz „HomeItem”-mel címezhető integer típusú elem. Tanuló
periódusban ez is egy tipikus hiba szokott lenni, hogy számítunk az előre beállított session adatra. De
ezt ne tegyük! Nem szentírás, hogy lesz tartalma például egy javascriptből indított oldallekérés esetén,
amit fél óra semmittevés után kezdeményez a felhasználó. Tapasztalatom, hogy ami ennyire egyszerű
és nem típusos, azzal legtöbb esetben baj szokott lenni. Az első leggyakoribb probléma, hogy mivel a
Session elemre stringgel hivatkoztam („HomeItem”) fennáll a veszélye, hogy majd máshol is
megteszem - azonos névvel - egy másik kontrollerben és így agyonvágom a saját adataimat. Ez könnyen
előfordulhat (láttam már ilyet sokat), ha egy jól sikerült kontrollert copy+paste –el lemásolnak, mert
feleslegesen gépelni senki se szeret. A másik probléma, hogy nem típusos ezért állandóan castolni kell,
ha meg akarjuk kapni az értékét, mert a Session kollekciójának elemei object típusúak.
A fenti két problémára egy védelem, ha a Session kezelését típusossá és egységessé tesszük, mondjuk
kontroller szinten:
return View();
}
[Serializable]
public class HomeSessionData
{
public int PreviousId { get; set; }
public int[] VisitedProducts { get; set; }
}
Ezzel a kódot is átláthatóbbá tettük. A legfontosabb, hogy a Session kontrollerfüggő elemét egy helyen
a HomeSession propertyben típusossá alakítva érhetjük el. A másik célszerű megoldás, hogy ne minden
egyes alaptípushoz (int, string, DateTime, …) készítsünk egy új session bejegyzést, hanem csomagoljuk
ezeket egy tároló osztályba (HomeSessionData). Ha tudjuk, hogy egy Session bejegyzésre már nem lesz
többet szükségünk, akkor adjunk neki null-t, hogy ne kelljen az ASP.NET-nek ezzel tovább foglalkoznia
és memóriát fogyasztania. A session bejegyzés nevét a fenti példában a kontroller osztály nevéből
kapja, ami elégséges, amíg nincs valahol még egy HomeController-ünk. Számos blog, cikk és megoldási
5.4 A kontroller és környezete - Controller 1-93
A fenti példák azt sugallják, hogy a Session a memóriában tárolódik. Az alapbeállítás szerint
igen, de meg kell említeni, hogy nagyobb alkalmazásoknál, amelyek kiszolgálásában több
webszerver is részt vehet, lehetőség van a Session adatokat más gépen tárolni. Erre az egyik
lehetőség, hogy a Sessiont a másik számítógép memóriájában tároljuk, ekkor hálózati
kapcsolaton keresztül érhetjük el az adatainkat. Egy másik lehetőség, hogy az adatokat a másik
gép SQL szerver adatbázisában irányítjuk. Ebben a két esetben csak olyan objektumot tudunk
tárolni a Session-ben, amelyek sorosíthatóak, ezért van az előbbi példában a HomeSessionData
osztály [Serializable] attribútummal kidekorálva. Bonyolult, sok tulajdonsággal, listákkal
rendelkező osztályt nem érdemes használni a sorosítás-visszaalakítás költsége miatt.
A Session-nek lejárati ideje van, tehát, ha ez alatt az idő alatt nem használjuk az oldalainkat,
akkor a tartalma törlődni fog.
Ha viszont lejárati ideje van, akkor legalább addig foglalja a memóriát. Ez pedig véges. Tehát
ne tároljuk benne sokáig sok adatot. (Emlékeztetőnek: minden új felhasználó egy új böngészési
folyamat jelent és új session-t nyit, tehát még ezzel is meg kell szorozni). Hogy mennyi a
maximum adatmennyiség az az alkalmazástól és a használók számától függ, de adatbázis
lekérdezések több tízezer soros eredményét ne tároljuk benne.
A Session mellett ott van még egy nagyon hasonló tároló a Cache, amit sok esetben előnyösebb
használni. Sőt olyan vélemények is vannak, hogy a Session-t inkább el kell felejteni és csak a
Cache-t használni, de ebbe most ne menjünk bele.
<configuration>
<system.web>
<sessionState mode="Off" />
</system.web>
</configuration>
A példa szerint a mode attribútummal lehet szabályozni a session adatok tárolási helyét. A mode
attribútum lehetséges értékei
Egy gyakran fontos beállítási lehetőség még a timeout. A session lejárati idejét 60 percre állítja át az
alapértelmezett 20 percről ez a web.config definíció:
Session_Start – Az után következik be, amikor a Session objektum létrejött, de még nem lett beállítva
az action kódban semmi. A session akkor is létrejön, ha olvasni akarunk a Session objektumból, ezért
jó hely arra, hogy az alapértelmezett adatokkal feltöltsük azt. Ezzel elkerülhetők az olyan helyzetek,
hogy egy action session adatra vár, de az nincs beállítva. (amivel az About actionös példában riogattam)
Session_End – Lefut, mielőtt a session felszámolásra kerül, azaz manuálisan lett törölve
(Session.Abandon()) vagy lejárt a timeout-ja. Itt még lehetőség van felszabadítani a tartalmát, vagy
esetleg elmenteni valahová. Amolyan csináld magad SQL state server módozatban megoldható, hogy
a „kosár” tartalma mégse vesszen el egy 30 perc ebédidő alatt. (Ami nagyon tudja bosszantani a
felhasználókat az az, ha újra össze kell szednie a vásárlási listáját.)
SessionStateBehavior.Required – Az előző ellentéte, engedi a session írását és olvasását is. Ennek akkor
van értelme, ha egyébként a web.config session beállítása nem tenné lehetővé azt.
5.5 A kontroller és környezete - Action és paraméterei 1-95
Valamint ajánlatos, hogy a visszatérési értéke ActionResult leszármazott legyen vagy null.
A Kontroller alfejezetben már nagyjából bemutattam az action szerepét és a 7. ábra már néhány
lényegi elemet bemutatott. Most sokkal részletesebben szeretném bemutatni, hogy milyen képességei
vannak az MVC rendszernek.
13. ábra
A template-k közül válasszuk ki az ’MVC controller with empty read/write action’-t. Az MVC régebbi
verzióiban ez volt a kontroller sablon. Az eredmény egy szokásos kontroller lesz, amiben a leggyakoribb
műveletek szerepelnek. (Index, Details, Create, Edit, Delete)
Csináljuk meg ugyanezt a Details és az Edit actionökkel is. Az Edit-ből csak egy kell , hiába van két
metódus a kontrollerben.
Az action metódusok feletti megjegyzésben ott szerepelnek, hogy az action milyen URL path-al érhető
el. Ha megnézzük a Details(int id) metódust láthatjuk, hogy az '/ActionDemo/Details/szám' lesz az URL
vége. A routing-nál mutattam, hogy ez azért lehetséges, mert van egy olyan bejegyzésünk, mint amit
a 9. példakód mutat. Az ’ id = UrlParameter.Optional’ a routing bejegyzésben azt mondja, hogy a
Details(int id) metódusunknak nem is kell id, mert opcionális. Ezt próbáljuk is ki. Indítsuk az alkalmazást
és az URL path-nak adjuk meg a /ActionDemo/Details –t. Erre szép hibaüzenetet kapunk:
The parameters dictionary contains a null entry for parameter 'id' of non-nullable type 'System.Int32'
for method 'System.Web.Mvc.ActionResult Details(Int32)' in
'MvcApplication1.Controllers.ActionDemoController'. An optional parameter must be a reference
type, a nullable type, or be declared as an optional parameter.
Parameter name: parameters.
Tehát nem opcionális. Ugyanis az a routing mintának szól. Az action paraméterét úgy tehetjük
opcionálissá, amint az előbbi hibaüzenet is mondja, hogy nullázható típusúként definiáljuk a
paramétert.
Így már mennie kell. Hozzáteszem, hogy ez és a következő példák közelebb vannak a játszadozáshoz,
mint a valódi alkalmazáshoz, mert mi értelme lenne egy Details View-nak ha nem közöljük, hogy melyik
entitásról van szó.
// GET: /ActionDemo/Details/5
public ActionResult Details(int id = 0)
{
return View(id);
}
A példában az id-t paraméterként adtam tovább a View metódusnak. Most az id, mint egy integer típus
képezi az egész modellt. Hogy láthassuk is az eredményt az ActionDemo/Details.cshtml fájl tartalmát
írjuk át ilyesfélére:
@model object
@{
ViewBag.Title = "Details";
}
<h2>Details</h2>
@Model
Most bővítsük a metódus paraméterek számát és tegyünk bele két sort, ami a ViewData dictionary-be
belemásolja az opcionális category és format paraméterek tartalmát.
5.5 A kontroller és környezete - Action és paraméterei 1-97
Gyúrjuk tovább a Details.cshtml-t is, hogy lássuk az eredményt, és használatba vegyük az ősrégi
ViewData-t:
@model object
@{
ViewBag.Title = "Details";
}
<h2>Details</h2>
<br />
Modell: @Model
<br />
Kategória: @ViewData["kategoria"]
<br />
Formátum: @ViewData["formátum"]
Most azt szeretném, hogy a kategória ’Másik’, a formátum ’Szép’ legyen. Vajon így meg tudom adni?
/ActionDemo/Details/8/Másik/Szép
Az alapértelmezett route bejegyzéssel nem fog menni, ehhez módosítani kell egy ilyenre, ahogy arról
már volt szó:
routes.MapRoute(name: "Kategoriak",
url: "{controller}/{action}/{id}/{category}/{format}",
defaults: new { controller = "Home", action = "Index",
format = UrlParameter.Optional,
category = UrlParameter.Optional,
id = UrlParameter.Optional }
);
Persze azt nem érdemes csinálni, hogy mindenféle helyzetre felkészülve sok-sok route mappinget
csinálunk, mert átláthatatlan lesz. Ha beírtuk a fenti route bejegyzést és sikerült is kipróbálni a hosszú
URL-lel, ezek után töröljük vagy kommentezzük ki, hogy meglássuk, milyen hatással vannak az URL
paraméterek vagy más néven a query string-ek.
És hogy világos legyen, hogy mi megy végbe a háttérben, használjuk ezt a Details action változatot az
előbbi URL-lel kipróbálva:
Az eredmény azonos lesz. Ez a nyers valóság. Az ASP.NET rendelkezésre bocsájtja számunkra a Request
objektumot, amit kicsivel előbb megnéztünk, hogy a controller példányon is elérhető. Ebben
elérhetjük a query string lebontott név-érték párjait. Az MVC frameworkben található model binder,
ami a metódus paramétereinket megpróbálja összepárosítani a Request-ben található értékekkel. Ne
sajnáljuk rá az időt és egyszer alaposan nézzük végig, hogy mi is található ebben a Request-ben
futásidőben. Tettem egy breakpointot a Details metódusba, és miután megállt, a Request-en állva a
QuickWatch –al nézve (Ctrl+D utána Q) nálam megjelenő lista így nézett ki:
14. ábra
A request paraméterek (Params) száma nálam 71 volt. Ebben sok más is benne van nem csak az URL
paraméterek. Az MVC egyik fő feladata, hogy ezt a méretes Request objektumot könnyen kezelhetővé
tegye.
5.6 A kontroller és környezete - Az action kimenete, a View adatok 1-99
Az előbb megnéztük a ViewData tárolót és egy egyszerű modellt is használtunk. A View számára
biztosítani kell azokat az adatokat, amelyekből előállíthatja az oldal dinamikusan változó részeit.
Igazából több lehetőségünk van, némelyik elég furcsa lehet elsőre.
Modell
Ez nyilvánvaló, ha MVC a minta. Egyszerűen a controller ősosztályon levő View vagy PartialView
metódus paramétereként a View-hoz kerül. A tárolási helye a ViewData-ban van.
ViewData
Talán az eddigiek alapján már kiderült, hogy ez egy dictionary, ahol az elemeinek az indexelése string
alapon történik az elemei pedig objectek. Tehát nem típusos és így nem is nagyon szoktuk szeretni, ha
már a C#, mint típusos nyelv a programozási alap. Bár a típusának a neve ViewDataDictionary, de nem
teljesen csak egy dictionary, mert van neki többek között egy Model, ModelState és ModelMetadata
nevű tulajdonsága is.
ViewData
o Model
o ModelState
o ModelMetadata
A ModelState tárolja a validációs állapotot, input adatonként. A Model hordozza a View-nak átadott
modell objektumot, azt például amit a View(modelpéldány) metóduson keresztül továbbítottunk. A
ModelMetadata pedig a modell propertyjeinek leíróadatait más néven metaadatait tárolja. Itt
lelhetőek fel a modell propertykre aggatott attribútumok kiértékelt eredményei.
A ViewData tartalma végigkíséri az egész oldalfeldolgozást, de a HTML oldal generálása után elveszik.
Most a kontroller a téma, de elérhető a View-ból is és a hamarosan szóba kerülő action filerekből is.
Emiatt a ViewData nem csak abban View-ban érhető el, ami konkrétan az actionhöz kötődik, hanem a
_layout-ban és az összes PartialView-ban, amelyik részt vesz az aktuális oldal létrehozásában. A View-
ból lehet indítani további child actionöket, ebben az esetben az már egy másik ViewData lesz, ami
azokban megjelenik.
ViewBag
Nem olyan régen a C#-ban bevezetésre került a dinamikus típus. Ennek a dinamikus típusú tárolónak
a használatával nem kell bíbelődnünk a ViewData-val és a string alapú indexeléssel. Használhatjuk ezt
is. Nézzük meg a következő kódot, ami mutatja, hogyan is kell használni, és tippeljük meg mi lesz az
egyforma1 és egyforma2 lokális változó tartalma.
ViewBag.kategoria = "WBag";
bool egyforma1 = ViewBag.kategoria == ViewData["kategoria"];
ViewData["formátum"] = "Forma";
bool egyforma2 = ViewBag.formátum == ViewData["formátum"];
Igen, = true. Tehát a ViewData és a ViewBag azonos adatot szolgáltat. Referencia típusokkal is! Csak
azért mutattam meg, hogy a ViewData-t és a ViewBag-ot nem célszerű egymás mellett egy projektben
5.6 A kontroller és környezete - Az action kimenete, a View adatok 1-100
használni, mert ritka fura problémákat tud okozni, ha nem tudjuk, hogy a ViewData index neve és a
ViewBag dinamikus property neve azonos adatot jelent. A ViewBag-en keresztül természetesen csak
az egyszavas ViewData indexű elemeket érhetjük el, mivel a ViewBag property neve csak egy
összefüggő szó lehet. Viszont a ViewData indexelőjében nem kötelező az egyszavas indexkulcs.
TempData
HttpContext.Items[] gyűjtemény
Ide kívánkozik még ez az ideiglenes tárolási lehetőség, ami átível a request feldolgozásának a lépésein.
Ez szintén egy kulcs-értékpáros dictionary, ami elérhető a teljes request feldolgozása során. Szemben
az előző három lehetőséggel, nem csak a kontroller környezetének megszületése után (actionben,
filterben) használható, hanem már a request feldolgozásának a legelején a global.asax-ban is lehet
értékkel feltölteni. Például a
futásakor már elérhető. Ezt az Items gyűjteményt az alap ASP.NET keretrendszer biztosítja, tehát nem
MVC specifikus lehetőség.
A 6.4 fejezetben még egyszer visszatérünk az első három tárolónak a működésére és megnézzük a
használati eseteit.
5.7 A kontroller és környezete - ActionResult 1-101
5.7. ActionResult
Ahhoz, hogy az action metódus végén az összeállított adatokat el tudjuk küldeni - jellemzően azért,
hogy egy View sablon alapján HTML kód generálása legyen belőle - akkor ezeket az adatokat egy
szabványos ActionResult ősből származó típusba kell csomagolni. Ez a felparaméterezett
parancscsomag rendelkezik egy ExecuteResult metódussal, ami majd pontosan levezényli, hogy mi és
hogyan kerüljön a response kimentre, és végül a HTTP válaszba. Az ActionResult és leszármazottai:
System.Web.Mvc.ActionResult
System.Web.Mvc.ContentResult
System.Web.Mvc.EmptyResult
System.Web.Mvc.FileResult
System.Web.Mvc.HttpStatusCodeResult
System.Web.Mvc.JavaScriptResult
System.Web.Mvc.JsonResult
System.Web.Mvc.RedirectResult
System.Web.Mvc.RedirectToRouteResult
System.Web.Mvc.ViewResultBase
Ezeknek a visszatérési típusok többségének van egy-egy generátor metódusa a controller ősosztályon.
EmptyResult
Ahogy a neve is mondja, a böngészőnek csak annyit küld vissza, hogy OK (200-as http státusz), de
tartalmat nem. Ezt általában javascript kódok kérdésére szokták küldeni, ha nincs a kérésnek megfelelő
adat, viszont egyéb hiba sincs. Az alábbi két változat azonos eredményt ad csak az első kicsit
beszédesebb. A kódban persze nem fog lefutni a 2. változat, mivel az első return-al kilép a metódusból.
A következő példákban is ezt a módszert fogom alkalmazni.
//2. változat
return null;
}
5.7 A kontroller és környezete - ActionResult 1-102
ContentResult
//2. változat
return new ContentResult() { Content = txt };
//3. változat
Response.Write(txt);
return null;
}
A kód eredménye, hogy a txt változóban megadott tartalom megjelenik a HTML <body> -ban.
JavaScriptResult
Ez egy fura lehetőség, mert mindössze annyit csinál, hogy a Script propertyjében megadott szöveget
kiküldi a böngészőbe ’application/x-javascript’ contentType –al. Azaz olyan mintha egy ContentResult-
ot küldtem volna, de ’application/x-javascript’ –re beállított ContentType property tartalommal.
5.7 A kontroller és környezete - ActionResult 1-103
Példakódok:
Az első esetben a fájl tartalmát küldjük vissza, úgy hogy a tartalmat nekünk kell összeállítani egy byte
tömbben. Fontos paramétere a metódusnak a contentType - ami a fenti példában ’image/PNG’ -
ugyanis ez tájékoztatja a böngészőt a fájl típusáról, jelen esetben arról, hogy amit küldünk egy PNG
szabványú kép fájl. A contentType-ban megadandó szöveg az un. MIME26 típust jelöli, ami a szabványos
internetes tartalmakat azonosítja.
A második példában, a FilePathResult esetén, a szerveren található (olvasható) fájlt kell megadni
elérési út alapján. Ezt küldi a böngészőnek.
A harmadik példa pedig egy Stream –et vár, aminek a tartalmát küldi a böngészőnek. Nagyméretű fájl
esetén ezt a módot érdemes használni.
A példákban a FileDownloadName –ben egy fájlnevet megadva a legtöbb böngészőt arra készteti, hogy
ne próbálja megjeleníteni a tartalmat, hanem dobjon fel egy „fájlmentés” (Save as..) dialógusablakot,
hogy a fájlt a gépünkre tudjuk menteni. A példakódban szereplő ’Server.MapPath’ metódus, a mi web
alkalmazásunkon belüli relatív útvonalat, a szerveren fájlrendszerében értelmezett valódi/abszolút fájl
elérési útra alakítja. A ’~’ tilde karakter jeleni a site-unk gyökér mappáját.
JsonResult
JSON formátumú szöveges tartalmat küld vissza. A tartalom a Data propertyben megadott .Net
objektum JSON formátumú sorosításából áll elő. Az objektum nem a .Net szokásos sorosítási eljárás
alapján lesz feldolgozva, ezért a [Serializable] attribútumot sem kell használni. A tartalom típusa
’application/json’ lesz.
26
http://en.wikipedia.org/wiki/Internet_media_type
5.7 A kontroller és környezete - ActionResult 1-104
Ezt a JSON sorosított formátumot a böngészőben futó javascript kód könnyen javascript objektummá
tudja alakítani és felhasználni. Érdemes megjegyezni, hogy a JSON serializáció és deserializáció nem
olyan triviálisan egyszerű, és nem mindig hatékony, ha a beépített JsonResult –ot használjuk. Ezért erre
a problémára több alternatív megoldás is született, amik NuGet csomagok formájában érhetőek el.
További érdekesség, hogy a fenti példában definiáltam egy JsonModelClass osztályt, de valójában erre
nincs mindig szükség, mert használhattam volna egy anonymous osztályt is a Data értékeként:
HttpUnauthorizedResult
Azt közli az MVC-vel, hogy a felhasználó nem hitelesített, így az ASP.NET + MVC megpróbálja
hitelesíteni. Ez VS Internet Application template alapon létrejött alkalmazásnál azt jelenti, hogy a
felhasználót a web.config –ban megadott <forms loginUrl="~/Account/Login" /> login oldalra fogja
irányítani a böngészőt egy HTTP 302-es temporary redirect-el. Azaz a felhasználó az iménti beállítás
szerint a /Account/Login oldalon fogja magát találni.
HttpStatusCodeResult
Ennek segítségével egyedileg megadható, hogy milyen HTTP státusz kód kerüljön vissza a böngészőnek.
A leggyakoribb státuszkódokhoz készültek speciális leszármazottak. Ilyen például a
HttpNotFoundResult, ami 404-es kódot küld vissza. A következő két ActionResult is ilyen specializált
változat.
5.7 A kontroller és környezete - ActionResult 1-105
RedirectResult és RedirectToRouteResult
Ezek a böngészőnek egy temporary redirect-302 (átirányítás másik URL-re) választ küldenek. A
példakódban a GetRedirectResult metódus eredményeként az átirányítás miatt az index.hu oldala fog
megjelenni.
//2. változat
return new RedirectToRouteResult("Default",
new RouteValueDictionary(new { action = "GetContentResult", controller = "ActionDemo" }));
}
Ennek eredményeként a GetContentResult action hajtódott volna végre, mert azonos URL path-on van,
mint a konkrét hívás. De ha egy másik controller actiont kéne megadni (pl. Home/About), akkor
gondolkozni kellene, hogy mit is írjak URL-ként. A RedirectToAction controller metódus ennek terhét
veszi le rólunk, mert egyszerűen csak az Action és Controller neveit várja. Ez nagyon hasznos lehet, ha
az oldalaink menüstruktúrája még nem végleges és ide-oda kerülnek az actionök. A 2. változat elárulja,
hogy mi is történik a háttérben, a RedirectToAction esetében. Az eredménye azonos. Érdemes átnézni
a controller ’Redirect’-el kezdődő metódusait, mert további speciális lehetőségeket is rejtenek.
Az eddigi ActionResult-ok közös jellemzője, hogy valójában csak a Response objektumot töltögetik,
elrejtve a (nem túl nagy) nehézségeket előlünk, de alig csinálnak valami komolyat. Az MVC igazi erejét
a következő ActionResult leszármazottak hozzák a felszínre.
5.7 A kontroller és környezete - ActionResult 1-106
ViewResult
Ezzel az ActionResult-al már találkoztunk, hisz a kontroller View metódusai (szintén erősen túlterhelt
metódusok) ezt állítják elő. Íme, néhány példa a használatra:
//2. változat
return View(model);
//3. változat
return View("Index", model);
//4. változat
return View("Index", "_Layout", model);
//5. változat
return View("Index", "_Layout");
Az első – modell nélküli - változat feltételezi, hogy létezik egy ’ Index.cshtml’ View fájl (jelen esetben)
a projektünk Views/ActionDemo/ mappájában. Ezért, ha nem adunk meg View nevet, akkor az aktuális
metódusnév+.cshtml lesz a fájl név és az aktuális kontroller nevét veszi elérési útnak a Views mappán
belül.
PartialViewResult
Többször is esett már szó arról, hogy a beérkező request és route bejegyzések alapján a kontroller
meghatározásra és példányosításra kerül, majd meghívásra kerül a megfelelő action metódus. Az
általános helyzet, hogy a felhasználó elnavigál egy olyan oldalra, ahol egy űrlap fogadja, amit kitölt és
visszaküldi a szervernek. A lenti példában ez legyen az /ActionDemo/Edit/5 az URL vége.
// GET: /ActionDemo/Edit/5
public ActionResult Edit(int id)
{
return View(ActionDemoModel.GetModell(id));
}
// POST: /ActionDemo/Edit/5
[HttpPost]
public ActionResult Edit(int id, FormCollection collection)
{
try
{
ActionDemoModel fpdm = ActionDemoModel.GetModell(id);
if (TryUpdateModel(fpdm)) //Modell propertyk beállítása
return RedirectToAction("Index"); //Ha sikeres akkor az Index oldalra navigáltatunk
return View(fpdm);
}
catch
{
return View();
}
}
Az MVC az első Edit(int id) metódust fogja meghívni és az URL végén levő 5 pedig átadódik az ’id’ nevű
paraméterben. A View-ban ilyenkor egy HTML formot kell meghatározni. (A @*…*@ további, most
nem fontos kódot jelent.) A BeginForm-ról még lesz szó, a feladata egy HTML form generálása.
@using (Html.BeginForm()) {
<div class="editor-label">
@Html.LabelFor(model => model.FullName)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.FullName)
@Html.ValidationMessageFor(model => model.FullName)
</div>
@*. . .*@
<p> <input type="submit" value="Save" /> </p>
}
Ha ezt a formot a Save gomb megnyomásával (HTTP POST formában) visszaküldjük a szervernek, akkor
az Action kiválasztásnál most nem az Edit(int id), hanem az Edit(int id, FormCollection collection)
metódus fog meghívódni. Ennek az oka, hogy a [HttpPost] attribútum azt közli az MVC-vel, hogy amikor
az URL-nek megfelelő Action metódust próbálja kikeresni, akkor azt a dilemmát, hogy két ’Edit’ nevű
metódus is megfelel az URL path szerint a kérésnek, úgy oldja fel, hogy a POST típusú HTTP requestet
a második (Edit(int id, FormCollection collection)) metódushoz irányítja.
HttpGet, HttpDelete, HttpHead, HttpOptions, HttpPath, HttpPut, ezek mind az azonos nevű HTTP
metódus szerint választják ki az actiont. Ezeknek a szervezett használatával elérhetjük, hogy a
5.8 A kontroller és környezete - Action kiválasztása 1-109
rendszerünk kövesse a REST filozófiáját és megtehetjük, hogy az azonos adat reprezentánssal (termék,
személy) rendelkező tartalmakat a HTTP method szerint kezeljük.
Mint említettem ez szervezési kérdés, tehát nincs kőbe vésve, hogy a HttpPut-al csak új adatot lehet
létrehozni, a HttpPost pedig csak adatmentésre használható. Lehet éppen fordítva is (ez a http eredeti
koncepciója, csak a gyakorlat mást hozott). A legtöbb MVC megvalósításban szinte csak a HttpPost,
ami fellelhető, mint action kiválasztó attribútum. Egyrészt, mert a Get a feltételezett alapértelmezett
kiválasztás, akkor is, ha nem rakjuk rá a metódusra. Másrészt, mert a HttpPost-al lefedhető az
adatmanipuláló műveletek action metódusba való szervezése, metódus név szerint. Gondoljunk bele,
hogy mennyivel beszédesebb azt írni a metódus névnek, hogy DeleteTermek(int id), minthogy valami
közös nevet adunk neki az Edit funkcionalitású metódussal és csak a HttpDelete attribútum közli, hogy
valójában a metódust törlésre használjuk. Így azonban elvesztük a REST jellegű URL formát, viszont
esetleg kedvezünk a felhasználóknak. Mikor mi a fontosabb.
//Azonos nevek, csak a HTTP method neve különböztet meg. API jellegű REST megvalósítás
[HttpGet]
public ActionResult Edit(int id) { }
[HttpPost]
public ActionResult Edit(int id, Model model) { }
[HttpDelete]
public ActionResult Edit(int id, bool allow) { }
[HttpPost]
public ActionResult EditProduct (int id, Model model) { }
[HttpDelete]
public ActionResult DeleteProduct(int id) { }
Egy árnyalattal jobb védelmünk lesz, ha a DeleteProduct(int id) metódusra rárakjuk a HttpDelete
attribútumot, mert ezt még véletlenül sem fogja a felhasználó elérni a böngészője címsorában
megadható URL-el. (mivel az HTTP Get metódussal próbál hozzáférni az erőforráshoz)
Az AcceptVerbsAttribute(HttpVerbs verbs) pedig lehetőséget ad arra, hogy egy action metódus több
HTTP method-nak is felelőse legyen.
A NonActionAttribute azt közli az MVC-vel, hogy az ezzel kidekorált metódus ugyan részt vesz az action
keresgélős játékban, de meghívni nem lehet, mert hibával elszáll a program. Ennek az attribútumnak
akkor jut szerep, ha a kontrolleren publikus (nem statikus, actionnek használható) metódust
definiáltunk, de biztosítani akarjuk, hogy ne lehessen elérni URL alapján, úgy mintha egy action lenne.
Ezzel például kizárhatjuk ideiglenesen a használat alól a fejlesztés félkész állapotában. Ennél is nagyobb
a haszna, amikor a kontrollerünk belső működését szeretnénk unit tesztelni, például egy speciálisan
paraméterezett metóduson keresztül.
[ActionName("Szerkesztes")]
public ActionResult Edit(int id)
{
return View(ActionDemoModel.GetModell(id));
}
Figyelem, ilyenkor a keresett View a Szerkesztes.cshtml lesz és nem az Edit.cshtml, ha nem határozzuk
meg explicit a Controller.View metódusban a nevet!
Annyi biztos, hogy az Action kiválasztásához az MVC-nek tudunk tippeket adni attribútumon keresztül.
Az attribútumokkal történő varázslatok listája még nem teljes, ugyanis az actionök és a kontrollerek
számára további működést befolyásoló beépített lehetőségek állnak rendelkezésre un. action filterek
formájában.
5.9. Filterek
Az action metódus meghívása előtt, közben és a metódus elhagyása után az MVC infrastruktúrája
lehetőséget biztosít további beavatkozásra néhány alap attribútum típussal, amit aztán igény szerint
felülbírálhatunk. Ez egy nagyszerű lehetőséget biztosít az actinobe kerülő kódismétlések elkerülésére.
A filterek kódjában akár azt is eldönthetjük, hogy a szóban forgó action lefusson-e vagy ne és helyette
pl. egy HTTP redirect legyen a böngészőnek küldendő válasz. Mivel attribútumról és beavatkozásról
van szó, ami az osztály és/vagy egy metódus normál működést változtatja meg és ezt az alkalmazás sok
helyén szeretnénk használni, ezért a filterek használata alapos megfontolásokat szokott igényelni.
Kiváltképp, ha mi írunk egy újat. A filtereket négy fő kategóriába lehet sorolni.
Hitelesítés (IAuthenticationFilter)
Az MVC 4 erősen arra épít, hogy az ASP.NET alaprendszer képes teljesen lekezelni a hitelesítést. Ezzel
az interfésszel egy beavatkozási pontot kapunk arra, hogy speciális, egyedi hitelesítést tudjunk
implementálni. A Controller ősosztály is rendelkezik ilyen attribútummal, ezért nem csak
actionfilterből, hanem egyedileg a kontrollerből is le tudjuk kezelni a speciális hitelesítési igényeket.
Minden más filter felhasználása előtt, a OnAuthentication metódusa kerül meghívásra. Ebben
implementálhatjuk a saját hitelesítési logikánkat. Esetleg használhatjuk arra is, hogy minden más filter
előtt valami inicializálást végezzünk. Az MVC 5 jelenlegi állapotában már arra is lehet használni ez a
metódust, hogy a HttpContext.User és a futó szál felhasználóját (Thread.CurrentPrincipal),
pontosabban az IPrincipal–t megvalósító objektumot, ideiglenesen lecseréljük a request számára.
Elég jó lehetőség a fejlesztés során, amikor a mi fejlesztői tesztfelhasználónk helyett átmenetileg mást
szeretnénk helyettesíteni. Mondjuk, hogy lássuk mit látna ő, és mihez lenne joga, ha bejelentkezne.
(Anélkül hogy a jelszavát elkérnénk)
Hasznos a Result propertyje is, amivel egy ActionResult leszármazottat tudunk átpasszolni. Például
más oldalra irányítani a böngészőt a RedirectToRouteResult-al, ha a hitelesítésen elbukna. A Result
feltöltése megszakítja a request további feldolgozását, és akkor a filterek és az action nem fognak
elindulni.
Más nézőpontból: az IAuthenticationFilter két metódusa körülöleli a többi filtert azzal, hogy az összeset
megelőzve lefut a OnAuthentication, és az összes feldolgozása után még az
OnAuthenticationChallenge meghívásra kerül.
Ezeknek a filtereknek általában az a feladatuk, hogy az action metódus meghívása előtt ellenőrizzék,
hogy az action futtatható-e. Ezt az OnAuthorization(AuthorizationContext filterContext) metódus
implementálásában lehet eldönteni. Nincs visszatérési értéke. Ha úgy értékeli a kód, hogy az action
nem futtatható le, akkor dobhat egy exception-t és ezzel le is tudja a feladatát. A futási feltételek az
aktuális AuthorizationContext kontextus alapján szoktak kiértékelődni: a felhasználó joga, a HTTP
protokoll biztonsági szintje, a request érvényessége, stb. Ennek az AuthorizationContext
objektumnak van egy Result nevű tulajdonsága, amibe egy ActionResult elszármazottat tudunk tenni.
Mivel az ActionResult akár lehet egy RedirectResult vagy ViewResult is, így exception dobás nélkül is,
szépen lekezelhetjük a problémás esetet. A Result feltöltésével a request rövidre lesz zárva, nem kerül
végrehajtásra az action és a többi filter sem.
AuthorizationFilterAttribute
hozzáférhető-e a felhasználó számára vagy nem. Mivel ez az attribútum egy nagyobb témának a része
ezért ezt majd a 9.3 Felhasználó hitelesítés fejezetben nézzük meg részletesen.
RequireHttpsAttribute
Meghatározza, hogy az Action csak HTTPS protokollon keresztül érhető el. Érdekessége, hogy ha pl. az
GET request URL így néz ki: http://localhost/ActionDemo/CsakHttps, akkor át fogja irányítani a
böngészőt az azonos HTTPS oldalra: https://localhost/ActionDemo/CsakHttps. Csak Get-tel megy, a
POST-ot nem irányítja át, hanem exception-t dob.
ValidateAntiForgeryTokenAttribute
Egy biztonsági bélyeget használ, amivel azonosítható, hogy a form adatok, amit a post requestet
fogadó actionnek át kéne vennie, az valóban a mi alkalmazásunkból kiküldött megelőző request
alapján vannak kitöltve. Ezzel az attribútummal a 9. fejezetben fogunk részletesen foglalkozni.
ValidateInputAttribute
Ha a felhasználóknak megengedjük, hogy a felületen, pl. egy textbox-ban HTML + javascript kódokat
írjanak be, majd elmentjük és utána a kapott adatokat megjelenítjük egy áttekintő (detail) oldalon,
akkor megadjuk a lehetőséget, hogy ártalmas kódokat is becsempésszenek a HTML kimenetet generáló
rendszerbe. Ennek eredményeképpen egy ártalmas JS kód nyersen megjelenik a böngészőben és
aktivizálódik. Ennek kivédése az MVC alapértelmezett működése. Amit viszont ezzel az attribútummal
felülbírálhatunk, megnyitva a rendszerünket a rossz fiúk előtt. Mégis, szükség van rá egy CMS
rendszernél, ahol az a lényeg, hogy a felhasználók, híreket, blog bejegyzéseket töltenek fel, ami HTML
formázást és linkeket tartalmaz.
A potentially dangerous Request.Form value was detected from the client (Address="Budapest
<script>alert('Visze...").
Lazítsunk a rideg biztonsági rendszeren, tegyük rá ezt az attribútumot false paraméterrel az Edit
actionre:
[HttpPost]
[ValidateInput(false)]
public ActionResult Edit(int id, FormCollection collection)
{
try
{
ActionDemoModel fpdm = ActionDemoModel.GetModell(id);
if (TryUpdateModel(fpdm))
return RedirectToAction("Index");
return View(fpdm);
}
catch
{
5.9 A kontroller és környezete - Filterek 1-113
return View();
}
}
Kezd „jó” lenni, már el tudjuk menteni a formot, de még nem fut a cookie lopási lehetőséget
reprezentáló script, mert a potenciálisan veszélyes HTML tagek (<script>) helyett HTML entitások
formájában kódolva renderelte le a View.
El kell érni, hogy a modellben levő HTML tartalmat ne kódolja, hanem nyersen tolja ki a felhasználónak.
Ehhez a Detail.cshtml-ben az Address renderelését végző szakaszt le kell váltani a biztonságos
megoldásról a @Html.Raw(Model.Address) sor beiktatásával. Ott hagytam az eredetit is. (DisplayFor)
<div class="display-field">
@Html.DisplayFor(model => model.Address)
@Html.Raw(Model.Address)
</div>
ChildActionOnlyAttribute
Ahogy már említettem, és majd később a View-k tárgyalásánál látni fogjuk részletesen, az actionök
meghívása lehetséges a View-ból az Action() Html helperrel:
@Html.Action("DetailPartial")
Controller
Bármennyire is furcsa, maga a Controller ősosztály is megvalósítja ezt az interfészt. Lehetőségünk van
a saját kontrollerünkben felülbírálni az OnAuthorization virtuális metódust, és ezen belül - kontroller
szinten - megvizsgálni a filter kontextus alapján, hogy valóban elfogadható-e, hogy az action
végrehajtásra kerüljön. Sőt nem csak, hogy felülbírálja, hanem ennek a metódusnak lesz elsőbbsége
minden más IAuthorizationFilter megvalósítás előtt. Magyarul a Controller.OnAuthorization után
futnak csak le a normál attribútum alapú filterek. Ez a lehetőség akkor előnyös, ha a kontroller további
private segédmetódusokat használ, esetleg bonyolultabb, egyedi feltételrendszert megvalósítva, ami
nem általánosítható egy filter attribútumban.
ActionFilterAttribute
Fejlesztjük az MVC alkalmazásunkat, és eltelik pár hét. Írtunk egy sereg kontrollert bennük sok-sok
action metódussal. Tehát lett, mondjuk 50db actionünk. Ha ránézünk ezekre együttesen, majdnem
biztos vagyok benne, hogy nagyon sok kódismétlést fogunk találni. Azt pedig nem szeretjük, mert ezek
az ismétlések azt mutatják, hogy egy üzleti igényt újra és újra implementáltunk, vagy azonos kontextust
varázsolunk egy ORM framework számára. Mi van akkor, ha csak egy picikét is megváltozik a
megrendelő igénye? Írjuk át minden helyen egyesével? Példának okáért legyen az igény az, hogy
kódunk végrehajtása naplózott legyen, hogy az éles rendszeren is kideríthető legyen, hogy hol futott
hibára a kódunk és mik voltak a hiba körülményei. Ehhez kell egy naplózó alrendszer, amit most csak
elképzelünk. (Példaalkalmazásban: Services.SillyLogger) Ennek a naplózónak van 3 metódusa. Ez legyen
a kiinduló állapot
Figyeljük meg, hogy az Enter és ExtiMethod szövegesen várja a futó action nevét, ami nem olyan szép.
De ha mondjuk az EnterMethod megvalósítása úgy indul, hogy reflection-nal kikeresi, hogy milyen
metódusból hívták meg, akkor ez a paraméter elhagyható lenne. Ahhoz, hogy az EnterMethod
megvalósítása ilyen és még környezet érzékeny is legyen, tehát meg tudja különböztetni, hogy egy
Actionból hívták meg vagy más kódból, akkor már okoskodni kell.
5.9 A kontroller és környezete - Filterek 1-115
Egy action az 50-ből, amikben a loggolás hívása ismétlődik, tehát ezt kéne 50x leírnunk (sicc!):
Ráadásul még mérgelődhetünk is, mert csúnyán néz ki a metódus vége, ha még a View() visszatérési
értékét (ActionResult leszármazott) is naplózni akarjuk. (var result = View(), majd naplózás, majd return
result). De ez csak egy állatorvosi ló, nem de?
Tessék csak ráülni, akarom mondani kipróbálni! Meg fogjuk látni az action és a View feldolgozásának a
folyamatát is rögtön. Ugyanis a result.View –ban (3. sor alulról) azt szerettem volna látni, hogy az oldal
elkészítése után milyen eredményt kapok, vagy legalább melyik .cshtml fájl volt a kiválasztott template
vagy bármi. De csak egy null lesz benne. Az oka, hogy nem „volt”, hanem „lesz”. A View kiválasztása és
renderelése ekkor még nem történik meg, azt később az MVC framework fogja megtenni az
ActionResult alapján. Ez néha megdöbbenti a fejlesztőket. „Hogy-hogy nincs a View renderelése a
közvetlen felügyeletem alatt?”. Pedig nincs, mert a kontroller és a View szereplők igen rendesen el
vannak választva egymástól. A result tartalma, ami egy ViewResult, ami az ActionResult leszármazottja,
csak egy csomag, mint egy nagy láda, amibe beledobáljuk az alkatrészeket és a szerelési tervet, amit
majd az MVC a saját futószalagján fog később összeszerelni.
Remélem elég problémát sikerült összelapátolnom, hogy megoldást is érdemes legyen keresni hozzá.
Segítségünk lesz ebben az ActionFilterAttribute, vagyis nem ez, hanem egy leszármazottja, mert ez egy
absztrakt osztály:
//Action meghívása
Szövegesen:
Az OnActionExecuting lefut, mielőtt az Action metódus elkezdene dolgozni, így naplózhatjuk ezt a
tényt. Az ActionDescriptor tájékoztat minket az action néhány jellemzőjéről, mint például a nevéről.
Az OnResultExecuting metódus következik, amit most nem használunk a fenti példában. Ekkor a View
fájl kiválasztásba még be tudunk avatkozni. Sőt a ResultExecutedContext.Cancel-el még meg is
szakíthatjuk a View feldolgozását.
Marad az OnResultExecuted, ami az ActionResult feldolgozása után fut le. Ebben már minden elérhető.
A View is meghatározásra került már. Az ActionResult leszármazott ExecuteResult() meghívásával
időközben a View template a Response kimenetre írta a számára előírt tartalmat. Ez jelen esetben a
View renderelt HTML tartalma. (ViewResult).
A példában csak két dolgot emeltem ki az elérhető adatokból: a View nevét (result.ViewName) és a
View elérési útját (razor.ViewPath) a fájlrendszerben. A példaalkalmazásban implementáltam egy
fapados LogFilter kiegészítőt, ami a kiküldésre kerülő renderelt HTML tartalmat is beleveszi a
naplózásba. Ennek a felhasználása van kikommentezve.
5.9 A kontroller és környezete - Filterek 1-117
[Services.SillyLoggerActionFilter]
public ActionResult BusinessCriticalA()
{
//..
//Itt sok kód lesz
//..
Services.SillyLogger.Store("Ez egy szöveg, amit naplóztunk");
//..
//Itt sok kód lesz
//..
return View("BusinessCritical");
}
A View() metódusparaméterében azért van ott a ’BusinessCritical’, hogy ne kelljen egy új View-t csinálni
a kipróbáláshoz. Egyébként a BusinesCriticalA.cshml-t keresné.
Talán kezd érezhető lenni az MVC rugalmassága. Ezt a filtert ezután ráilleszthetjük mind az 50
képzeletbeli actionünkre és nagyon rugalmasak lettünk, mert a naplózást egy helyen javítgathatjuk. De
lehetünk még kényelmesek is.
Jaj, most jut eszembe, van még egy apróság (’ kezd az idegeimre menni Columbo ’), maga a Controller
ősosztály is egy "ActionFilter", persze nem attribútum csak megvalósítja az IActionFilter interfészt csak
úgy, mint az attribútum. Az MVC szempontjából csak ez a fontos. A controller alaposztálynak léteznek
5.9 A kontroller és környezete - Filterek 1-118
OutputCacheAttribute
Hibakezelés (IExceptionFilter)
HandleErrorAttribute
Ha egy actionben hiba történik, akkor az MVC alapértelmezett nagy sárga hibaüzenő oldala jelenik
meg.
<system.web>
<customErrors mode="On"/>
A View, ami ezt előállította a Views/Shared/Error.cshtml fájlban van. Ugye emlékszünk még, hogy az
App_Start/FilterConfig.cs fájlban van egy filter globális példány definiálva? Az ottani beállítás miatt a
HandleError attribútum érvényes és hatásos az alkalmazásunkban minden actionnel kapcsolatban. A
5.9 A kontroller és környezete - Filterek 1-119
paraméter nélküli HandleError úgy van beállítva, hogy az Error.cshtml View-t használja
hibamegjelenítésre. A modell igényét az első sorban találjuk:
@model System.Web.Mvc.HandleErrorInfo
@{
ViewBag.Title = "Error";
}
View – Meghatározhatjuk a hiba megjelenítő View-t. Akár a globális verzióban is és akkor nem
az Error.cshtml lesz a mindenes hibajelentő. Kontroller szinten, akkor kontrollerenként
készíthetünk hiba oldalt.
Master – Ezzel a hibamegjelenítő View layoutját, mester oldalát adhatjuk meg.
ExceptionType – Ezzel pedig, hogy az attribútum milyen Exception típusra és annak
leszármazottjaira reagáljon. Mivel az alapértelmezett beállítása az 'Exception' osztály, ami az
alaposztálya minden másnak, ezért mindenre reagál, ha nem adjuk meg explicit a típusát.
@model HandleErrorInfo
@{
ViewBag.Title = "HandleException";
}
Készítsünk egy MakeException actiont és egy további bug generátort MakeGeneralException néven:
Ha nem akarunk az Orderrel bajlódni, akkor úgy is lehet játszani a kiértékelési sorrenddel, hogy ami a
GlobalFilterCollection-ba később kerül bele, azt előbb fogja kiértékelni. Tehát ha a fenti hozzáadásokat
megfordítom, akkor az Order beállítása nélkül is jó lesz. Mondjuk, arra nem mernék mérget venni, hogy
minden esetben és minden későbbi MVC verziónál így lesz.
Nagyjából erre jók a filterek. A további részekben fogjuk még használni némelyiket. Ha valami nem
megfelelő számunkra a filterekkel kapcsolatban, akkor az egész filter kezelési mechanizmust
lecserélhetjük szőrőstől-bőröstül, ha regisztrálunk egy IAsyncActionInvoker/ IActionInvoker -t
megvalósító osztályt és abban úgy implementálhatjuk a filterek felhasználását, ahogy csak szeretnénk.
6.1 A View - A View mappák 1-121
6. A View
A View-k helye rögzített a projekt struktúrában. Amit, mint mindent megváltoztathatunk, de most ne
ezzel kezdjük, hanem azzal, ami adott.
A View-k a Views mappa almappáiban helyezkednek el. A szisztéma nagyon egyszerű. A Views
mappában vannak azok a mappák, amelyeknek a nevei azonosak a benne levő View-khoz tartozó
kontrollerek osztályneveivel (mínusz „Controller” végződés). Az ezekben levő fájlok nevei rendre
megegyeznek az Actionök neveivel, amik a kontrollerben vannak implementálva. Kivéve, ha használjuk
az action aliasznév képzésére az ActionName attribútumot.
Így a View fájljai kontrollereként jól vannak csoportosítva, ami átláthatóvá teszi a fejlesztést.
Vannak azonban olyan View-k is, amik nem kötődnek közvetlenül egy
kontrollerhez. Ezeket a Shared megosztott mappába érdemes tenni.
Itt található még a közös _Layout.cshtml sablon is.
6.2 A View - A View fájl kiválasztása 1-122
Láthattunk már példákat, arra, hogy az action metódus végén visszaadandó ViewResult objektumot
hogyan lehet felparaméterezni. Két alapvető lehetőségünk van. Az egyik, hogy explicit megadjuk a
View nevét (legtöbbször fájlkiterjesztés nélkül), a másik, hogy nem adunk meg nevet és az MVC az
action metódus nevét veszi a View nevének is. Azt is láttuk, hogy az action metódusra rá lehet tenni az
ActionName attribútumot, ami befolyásolja az action névszerinti kiválasztását és a View nevét is
meghatározza.
Gondolom, kezd megszokottá válni, hogy ebben az MVC-ben minden testre szabható. Természetesen
a View fájl kiválasztása is ilyen. Mielőtt belemélyednénk a részletekbe, tegyünk egy egyszerű próbát,
hogy meglássuk mi az alapértelmezett üzem. Írjunk egy olyan kontrollert egy olyan action metódussal,
amihez nem tartozik View (.cshtml) és érjük el a böngészőből. Ehhez csináltam egy kontrollert, és
elkészítettem a Views mappa alatt a ViewTest mappát, de nem csináltam View fájlt.
Azt kifogásolja, hogy nem találta a View-t (vagy a master View-t, a _Layout.cshtml-t) a következő
helyeken, egyik ilyen fájlkiterjesztéssel sem:
Tehát az MVC megpróbál mindent, csakhogy ne essünk kétségbe. Sajnos túl sokat is keresgél, mivel itt
most a projektben nem akarom írni sem VB.NET-ben a View template-be kódot (.vbhtml), sem ASP.NET
Web Forms szintaxissal a dinamikus tartalmat (.ascx, .aspx). Összesítve a 8 próbálkozási lehetőségből
6-ra biztosan nem lesz szükségem. Egy valós alkalmazásnál ajánlás, hogy a felesleges köröket
6.2 A View - A View fájl kiválasztása 1-123
minimalizáljuk, hogy egy picit gyorsabb legyen az MVC működése, azzal hogy ezt a keresési listát
leszűkítjük27. Kezdetnek, írjunk két sort a global.asax-ba:
Hibaüzenet: The view 'NonexistentPage' or its master was not found or no view engine supports
the searched locations. The following locations were searched:
~/Views/ViewTest/NonexistentPage.cshtml
~/Views/ViewTest/NonexistentPage.vbhtml
~/Views/Shared/NonexistentPage.cshtml
~/Views/Shared/NonexistentPage.vbhtml
Eltűntek a Web Forms szintaktikájú .as*x fájlok keresései! Az arány jobb, négy próbálkozásból már csak
kettőre nem lesz biztosan szükségünk: a VB.NET template-ek keresésére. Egyszerű munkával a
próbálkozások felét kiiktattuk, de ne elégedjünk meg ennyivel. A kaland kedvéért, keressük meg az
MVC forráskódjában a RazorViewEngine forrását és másoljuk ki a konstruktorát, majd csináljunk egy
leszármazottat belőle és annak konstruktorába csak azokat a keresési útvonalakat állítsuk be, amire
szükségünk van. Nálam így sikerült:
27
Egyéként sem lassú, mert a sikeresen megtalált View fájl elérési útját cache-eli
6.2 A View - A View fájl kiválasztása 1-124
Az eredmény magáért beszél, összesen két mappával próbálkozott az MVC: "ViewTest" és "Shared":
Hibaüzenet: The view 'NonexistentPage' or its master was not found or no view engine supports
the searched locations. The following locations were searched:
~/Views/ViewTest/NonexistentPage.cshtml
~/Views/Shared/NonexistentPage.cshtml
Érdemes megnézni a definíciókat. Nekünk a ViewLocationFormats tulajdonság átírása is elég lett volna.
A többi keresési útvonalat tároló property neve elég beszédes. Ezek definiálják, hogy a továbbiakban
megismerendő View típusokat (Partial, Master) hol keresse az MVC keretrendszer. A kísérlet
mellékhatása, hogy vérszemet kaphatunk és innentől a kezünkbe vesszük az egész View keresést és
úgy alakítjuk, ahogy akarjuk. A másik mellékhatása, hogyha ezek után sikertelen View keresésre utaló
hibaüzenetet kapunk, akkor tudjuk, hogy mit és hol keressünk.
Az előbbiekben kiderült, hogy az MVC a View cshtml fájlt két fontos helyen is próbálja megkeresni. Az
első hely a kontroller nevéből képzett mappa, a második a Shared mappa. Mivel a keresési logika ilyen,
ezért ha olyan View-ra van szükségünk, amit több kontrollerből is szeretnénk használni, akkor az
ilyeneket célszerű a Shared mappába tenni. A kontroller actionből nézve ez azt jelenti, hogy a return
View(”ViewNev”); sorba nem kell beírni a Shared elérési utat, mert meg fogja találni. A Shared
mappába általában a Partial View-kat szoktuk tenni.
Az MVC 4 egyik újdonsága, hogy képes a View kiválasztását eszközfüggővé tenni. Tudunk létrehozni
View variánsokat, amelyek a böngésző eszköztípusával fájlnév konvenció alapján összerendelhetők.
Most megnézzük az alapértelmezett működést és majd a 11.3 fejezetben alaposan áttanulmányozzuk
a további lehetőségeket.
Ez a sok-sok View fájlkeresgélés, főleg ha elsőre nem található, Shared-ben levő fájlról van szó nem
nagy megterhelés az MVC számára, csak az első fájlkeresés költséges. A megtalált útvonalat utána egy
20 perces csúszó lejáratú 28 cache-ben tárolja. Emiatt a további requestek estén nem indul el a
keresgélés újra a fájlrendszerben.
28
Sliding expiration: A lejárati idő fele után érkező további elérések továbbtolják a végső lejárati időt.
6.3 A View - Tartalma, típusos View 1-125
A View-(k)ban kell megfogalmaznunk az oldalunk kinézetét. A View fájl egy része statikus HTML, ami
közé kerülnek a dinamikus szakaszokat előállító kódok. Ha még megvan a 4.7 fejezetben definiált demó
modell, kérjünk egy helyi menüt a ViewTest
mappán (a kép alján). Használhatjuk az Add és
View… menüpontokat, mint ahogy a jobb oldali
ábra mutatja.
@Html.DisplayNameFor(model =>
model.FullName)
@Html.DisplayFor(model =>
model.FullName)
Ez egy nagy könnyítés, hogy a modellből úgy-ahogy létrehozza a View tartalmát. De nem teljesen jól
csinálta, mert a modellünknek van egy belső PurchasesList nevű listája, amivel nem csinált semmit.
Igazából azt ne is várjuk el, hogy a Details template a modell egyszerű típusain kívül mást is
legeneráljon.
6.3 A View - Tartalma, típusos View 1-126
Ezen kívül egy darabkát kell beszúrni az előbb elkészített Details.cshtml-be, ahol a listát szeretnénk
viszontlátni, hasonlóan a könyv elején levő névjegykártyás példánál:
Ennek a lényege a @Html.Partial(…), amivel elértük, hogy a listát tartalmazó DetailList.cshtml (Partial
View) beékelődjön a Details.cshtml-be. Másrészt a Details számára átadott ActionDemoModel
objektum PurchasesList lista tartalmát továbbítottuk a DetailList számára. Ez utóbbi modelligénye az
első sorából kiolvasható:
@model IEnumerable<MvcApplication1.Models.ActionDemoPurchaseModel>
Lehetséges és érdemes a View-t darabokra bontani aszerint, hogy az adott darab előállítása
Az adott View darab és a modell kapcsolata lehet a felsoroltak tetszőleges keveréke is. Az első négy
nagyjából azt is jeleni, hogy a fő View és a partial View-k azonos modellből táplálkoznak, és egy request
kiszolgálását egy lépésben teszik meg.
Talán a 4. példa kis is lóghat ezek közül, ha a tartalmának a lekérése eleve javascript kódból történik.
Ebben az esetben a fő oldal letöltése után a böngészőben összeáll az oldal (Document Ready), majd
ennek a „kész” eseménynek a hatására a JS kód visszaszól a szerver actionjének (URL-en keresztül),
hogy kéne még amaz a View darabka is. Azonban jó eséllyel ez az URL a fő oldal előállításáért felelős
kontroller egy action metódusa lesz.
Az 5. példában, azt feltételezzük, hogy a View egy további MVC szubszekvenciát indít el. Ebben az
esetben az MVC szintén példányosítja a hívott kontrollert, kikeresi az actiont, meghívja és legenerálja
a View darabkát. Olyan, mintha a felhasználó böngészője indított volna egy külön lekérést. De fontos
tudni, hogy ilyenkor addig nem kerül visszaküldésre a teljes response HTML tartalom, amíg ez az
alszekvencia le nem fut. Az egész a szerveren történik meg, egy munkaegységben. Az ilyen további
action hívás célmetódusát child actionnek nevezik az MVC terminológiájában. A child actionből nézve
a fő action környezetét és adatait, parent action névvel illetik. Létezik ez a lehetőség, azonban
meggondolandó a használata, mivel újabb kontroller példányosítása és kontextusának kitöltése is
megtörténik, ami plusz költség. Akkor is új kontroller példány jön létre, ha a child action történetesen
azonos kontrolleren van megvalósítva a kezdeményező actionnel (a parent actionnel). A teljesítmény
szempontjából nem ajánlott úgy felépíteni egy View-t, hogy sok (<2) child actiont indítson el. Azonban
ha az oldalunk működése megkívánja, a 4. AJAX-os esettel párosítva, igen hasznos tud lenni.
Gondoljunk arra, hogy a modellünktől teljesen független Partial View darabkát egyszer biztosan le kell
generáltatnunk. Ha ezt egy javascript kód a böngészőből indikálja, az időigényesebb, mintha a
szerveren egy (+child) menetben csináljuk meg. A Partial View darabka ezek után AJAX módon
frissítheti a saját tartalmát.
A következő bemutató egyben a beígért ViewData, ViewBag, TempData példakódja is. A kód a
PartialDemoController kiszolgálásában működik, ebből következően az URL path a /PartialDemo lesz.
Először is, itt a kód eredménye, amit próbáltam úgy összeállítani, hogy az egymásba ágyazódást is
mutassa. A felső blokkban a „Partial View eleje” kezdettel egy beágyazott normál partial View-t használ
(1. eset az előző listából). A „Child Action eleje” vezeti be a child action működését (5. eset). Mivel még
soha nem volt szükségem rá, érdekességképpen kipróbáltam, hogy lehet-e a Child actionnek további
Child actionje. (Természetesen igen)
6.4 A View - Partial View 1-129
Továbbá szeretnék kiemelni egy alig látható adatot, a „Tempdata: Elérhető”-t. Ez kizárólag a fő View
előállításáért felelős Index() metódusban lett beállítva, mégis azonos szinten érhető el mindegyik
szintű View-ban, még a child action View-kban is.
//ViewBag tartalma
ViewBag.Telefonszama = "+99 99-999-999";
ViewBag.EmailCime = "kukac@kukac.kc";
TempData["Tempadat"] = "Elérhető!";
return View();
}
return View();
}
Az Index.cshtml:
@{
ViewBag.Title = "Demo Index";
}
<style type="text/css">
.left { float: left;width: 30%;}
</style>
A DemoPartial.cshtml kódja:
<div>
<em>A Partial View belső, saját tartalma:</em> <b>Tempdata: @TempData["Tempadat"]</b>
</div>
<div>
<h5>Main ViewData</h5>
Szemelynev: @ViewData["Szemelynev"] <br />
Címe: @ViewData["Címe"]
</div>
@{
var parentViewData = this.ViewContext.ParentActionViewContext.ViewData;
}
<div>
<em>A sub action belső saját tartalma</em> <b>Tempdata: @TempData["Tempadat"]</b>
</div>
<div class="left">
<h5>1. Child ViewData</h5>
Szemelynev: @ViewData["Szemelynev"]
<br />
Címe: @ViewData["Címe"]
</div>
<div class="left">
<h5>Main ViewData</h5>
Szemelynev: @parentViewData["Szemelynev"]<br />
Címe: @parentViewData["Címe"]
</div>
<div class="clear-fix"></div>
<div style="background-color: #bbb; margin-left: 20px;">
<h3>*** 2. Child Action eleje ***</h3>
@Html.Action("SecondLevelChildAction")
<h3>*** 2. Child Action eleje ***</h3>
</div>
<div class="clear-fix"></div>
Azonban a child actionökkel az baj, hogy a böngésző felhasználót semmi nem akadályozza meg, hogy
az URL alapján is elérje a child actiont (már ha tudja mi az URL-je). Mivel az így indítandó action csak
az oldal kis részletének az előállításáért felel, nincs értelme közvetlenül URL alapon elérni. Ráadásul a
child actionöknél előfordul, hogy valamilyen egyéb kontextust is igényelnek, aminek a hiánya akár egy
exception is lehet. Ezért az összes ilyen actionnél nagyon ajánlott a ChildActionOnly attribútum
használata, amivel biztosíthatjuk, hogy child action metódusunkat csak View-ból lehessen meghívni:
[ChildActionOnly]
public ActionResult SubAction() { ….
A TempData felhasználását a legjobban az mutatja be, ha az aktuális action egy RedirectAction result-
al tér vissza. Ezt a funkciót az IndexRedir() metódus demózza.
Ha most a Tempdata demó újra linkre kattintunk, akkor a TempData tartalma már nem lesz elérhető,
csak az üres hely látszik:
Ha a most látottakat és a normál View fájl kiválasztásával foglalkozó részt összevetjük, felmerülhet a
kérdés, hogy ha a Partial View dinamikus megtalálásához csak egy View nevet adunk meg, akkor az
MVC csinál legalább egy felesleges keresési kört mielőtt a Partial View fájlra rátalál? Tegyünk egy
próbát és tanuljunk megint exceptionből. Átírtam a partial View helper paraméterét egy nem létező
partial View-ra:
@Html.Partial("DemoPartialNotFound")
Eredmény:
Exception Details: System.InvalidOperationException: The partial view 'DemoPartialx' was not found or no view
engine supports the searched locations. The following locations were searched:
~/Views/PartialDemo/DemoPartialNotFound.cshtml
~/Views/Shared/DemoPartialNotFound.cshtml
Miért is lenne másként? A Partial View-k keresése is próbálkozások sorozata. Először az aktuális
View, utána a Shared mappájában keresi. Ez akkor problémás, ha a Partial View történetesen egy
közösen használt és a Shared mappában található példány. Ekkor kényszerűen lefut egy biztosan
6.5 A View - A View-k egymásba ágyazása 1-133
sikertelen fájlkeresés a View saját mappájában. Erre az a megoldás, hogy pontosan megadhatjuk a
View fájl nevét elérési úttal együtt.
@Html.Partial("/Views/Shared/Kozos.cshtml")
return PartialView("/Views/PartialDemo/DemoPartial.cshtml");
A partial View keresési logikáját szintén a 13. példakód ConciseViewEngine megvalósításában levő
útvonal megadások szabályozzák jelen esetben. Illetve, ha nem bíráltuk volna felül a
RazozViewEngine konstruktorát, akkor az abban levő nyolc keresési eset zajlana le és nem kettő.
Az MVC szóhasználatában eddig beszéltünk View-ról, Layout-ról, Partial View-ról. Valójában ezek
között nincs akkora különbség. Mindegyik HTML template, bármelyikkel előállíthatnánk a teljes HTML
oldalt úgy, hogy a többiek üresek maradnak. Mégis jó, hogy tudunk felállítani 3 csoportszintet és ezzel
rendet tarthatunk és strukturálhatjuk, újrafelhasználhatjuk a View-kat.
_ViewStart.cshtml
Ebből egy szokott lenni a Views mappán belül. Ez a View összeállítási hierarchia csúcsa. Ezzel kezdi az
MVC. Amit ebben talál, az minden View számára alapértelmezett lesz. Elég, ha ennyi van benne:
@{
Layout = "~/Views/Shared/_Layout.cshtml";
}
@{ Layout = "~/Views/Shared/_Layout.cshtml"; }
<b>Minden oldalon akarom látni a nevemet: Regius Kornél.</b><br />
Eredménye, hogy igazán elégedett lehetek, mert oldalról oldalra vándorolva sem felejtem el a nevemet
.
6.5 A View - A View-k egymásba ágyazása 1-134
_Layout.cshtml
Ez a mester oldal. Ez a hordozója, kerete az összes valódi View-nak. A neve persze csak azért ez, mert
a _ViewStart-ban ezt mondtam, lehetne magyarosan _Mester.cshtml is. A feladata és a felépítése is
nagyon hasonló az ASP.NET Web Forms MasterPage-hez. Ebben lehet megfogalmazni a megjelenítés
azon elemeit, amit oldalról-oldalra egységesnek szeretnénk látni. Legyen az tartalom, elrendezés,
hivatkozott CSS és JS kódok, stb.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>@ViewBag.Title - View-k egymásban</title>
@RenderSection("htmlhead", required: false)
</head>
<body style="width: 600px; border: 1px solid #0000ff">
<h3>_LayoutDemo.cshtml tartalma</h3>
<div style="background-color: #eee; margin: 30px;">
<header>
@RenderSection("header", required: true)
</header>
<div id="body">
@RenderSection("featured", required: false)
@RenderBody()
@RenderPage("~/Views/ViewTest/EgybenPage.cshtml",
new { SzovegesTartalom = "Render page demó" })
</div>
<footer>
@RenderSection("footer", required: false)
</footer>
</div>
</body>
</html>
Ebből is látható, hogy ez a fájl tartalmazza a HTML keretsablont. Ebben benne vannak az előírt HTML
<html>, <head>, <body> elemek és a közé zárt szakaszok. A szakaszokon belül vannak elhelyezve az
HTML szekciókat renderelő razor funkciók:
@RenderSection - A megadott névvel ellátott View section szakaszt illeszti be az általa elfoglalt helyre.
A célja szinte teljesen azonos a Web Forms ContentPlaceHolder céljával.
Ha vesszük a @RenderSection("footer", required: false) sort, akkor ezt úgy kell értelmezni, hogy a
Layout-ot használó View-ban lehet egy footer section, ami nem kötelező (required:false).
@section footer
{
<h3>Egyben.cshtml FOOTER tartalma</h3>
}
A @section után jön a szabadon választott egyszavas név. A {} jelekkel közrezárt tartalom kerül a
RenderSection által képviselt helyre, mintha egyből oda írtuk volna. Ha required: true lenne, akkor a
6.5 A View - A View-k egymásba ágyazása 1-135
View-ban kötelező, hogy legyen adott nevű section. Az más kérdés, hogy ha nem rakunk semmit a
section blokkba, azt is elfogadja.
@RenderBody - A View-nak azt a szakaszát teszi ide, amelyik nem tartozik egyik View section alá sem.
Logikailag ebből az következik, hogy egy layout fájlban csak egy ilyen lehet.
@RenderPage – A megadott oldal renderelési eredményét illeszti be. Át tudunk adni egy anonymous
objektumon keresztül adatokat.
@RenderPage("~/Views/ViewTest/EgybenPage.cshtml",
new { SzovegesTartalom = "Render page demó" })
Ezt az objektumot a renderelt View Page propertyjében kapjuk vissza. (már megint egy adatcsomag):
Következzen a Layout, View, Partial View egymásba ágyazásának az eredménye, amiben szintén egyre
sötétebbek a tartalmak hátterei, ahogy mélyebben van a tartalom a hierarchiában:
A partial View-k nem rendelkeznek Layout propertyvel, így saját mesteroldaluk sincs, ennek
következménye, hogy hiába írunk bele @section {} szakaszt, annak a tartalma nem jelenik meg a
Layout-ban. Egy érdekesség, hogy a Layout.cshtml-nek is tudunk megadni felsőbb szintű Layout-ot,
mivel ez is csak egy View page. Ezzel egymásba tudjuk ágyazni a mesteroldalakat, ami nagyobb
alkalmazásnál hasznos tud lenni.
6.5 A View - A View-k egymásba ágyazása 1-136
@{
Layout = "~/Views/Shared/_LayoutDemoSub.cshtml";
}
@{
Layout = "~/Views/Shared/_LayoutDemo.cshtml";
}
@section htmlhead
{
<!-- A _LayoutDemoSub htmlhead beszúrt tartalma -->
@RenderSection("htmlhead", required: false)
}
@section header
{
<div style="background-color: #ccc; margin: 30px; border: 3px dotted #008000; padding: 5px">
<h4>A _LayoutDemoSub header beszúrt tartalma</h4>
@RenderSection("header", required: true)
</div>
}
@section featured
{
<div style="background-color: #ccc; margin: 30px; border: 3px dotted #008000; padding: 5px">
<h4>A _LayoutDemoSub featured beszúrt tartalma</h4>
@RenderSection("featured", required: false)
</div>
}
@section footer
{
<div style="background-color: #ccc; margin: 30px; border: 3px dotted #008000; padding: 5px">
<h4>A _LayoutDemoSub footer beszúrt tartalma</h4>
@RenderSection("footer", required: false)
</div>
}
<div style="background-color: #ccc; margin: 30px; border: 3px dotted #008000; padding: 5px">
@RenderBody()
</div>
_LayoutDemo.cshtml
o _LayoutDemosub.cshtml
Egyben.cshtml
Fontos meglátni, hogy a köztes Layout-nak a section-öket továbbítani kell úgy, hogy meg kell adni a
sectiont annak nevével, majd ebbe kell tenni a View számára szóló @RenderSection-t. Így ágyazódnak
egymásba a sectionök. Nem muszáj azonos section névvel továbbítani a végső View számára, úgy mint
a lenti példa mutatja (csak célszerű).
@section htmlhead
{
<!-- A _LayoutDemoSub htmlhead beszúrt tartalma -->
@RenderSection("htmlhead", required: false)
}
Hogy minden tiszta legyen, nézzük meg a „footer” section-nek a láncolatát. Ez van a legfelsőbb
_LayoutDemo.cshtml-ben:
<footer>
@RenderSection("footer", required: false)
</footer>
Ez a _LayoutDemoSub.cshtml-ben:
@section footer
{
<div style="background-color: #ccc; margin: 30px; border: 3px dotted #008000; padding: 5px">
<h4>A _LayoutDemoSub footer beszúrt tartalma</h4>
@RenderSection("footer", required: false)
</div>
}
@section footer
{
<h3>Egyben.cshtml FOOTER tartalma</h3>
}
Érdekes dolog, hogy a View-ban nem számít, hogy hova rakjuk a @section {} szakaszt, nincs sorrendhez
kötve. Nem függ attól, hogy milyen sorrendben definiáltuk a Layout fájlban a @RenderSection sorokat.
Csak a RenderSection a fontos, hisz oda fog a View section beékelődni, ahova raktuk azt.
Ez a layout láncolás akkor tud segíteni, ha a rendszerünk több nagy feladatkörre tagozódik (Értékesítés,
Ügyfél számára készült rendelést kiszolgáló oldal, HR, Vezetői oldalak, stb.), de ezek között is
6.5 A View - A View-k egymásba ágyazása 1-138
szeretnénk a minimális egységességet, pl.: hogy minden oldal tetején ott legyen a céglogó, a
bejelentkező név+jelszó beviteli mező. Az ilyen feladatkörre specializált (al)oldalcsoportok gyakran
eltérő javascript és CSS igényűek, ami miatt szintén jól jöhet a layout láncolás.
ViewNév.cshtml
Mostanra már szinte minden oldalról megnéztük a normál, hagyományos View-t. Tudjuk, hogy a
kontroller actionhöz kötődik, az oldal lényegi részét állítja elő. Láthattuk, hogy van neki egy Layout
propertyje, amivel meghatározhatjuk a Layout fájlt.
A Layout fájlt az action metódusban is meg tudjuk határozni a ViewResult paraméterezésével, ekkor
nincs szükség a View elején szokásos @{ Layout =”/layout/fájl/helye” } beállításra.
Eddig nagyjából össze-vissza használtam a Layout és a master (page) terminológiákat. Remélem nem
volt zavaró, de megtehettem, mivel ezek értelme azonos. Ha megnézzük az előbb használt és a
kontrolleren elérhető View metódus szignatúráját, ott masterName-nek hívják:
A HandleErrorAttribute-ban levő név: Master, mivel mint már említettem a hibakezelő View-nak
egyedi layout fájlja lehet.
Amikor elhagyjuk a kontrollert és átlépünk a razor View-k világába ott mindenhol a Layout elnevezést
fogjuk látni. A master elnevezésnek történelmi oka van, mivel az ASP.NET Web Forms-ban a közös
oldalsablont „Master Page”-nek hívják, és az első MVC keretrendszerek még a Web Forms renderelést
használták. Ha készítünk egy új MVC 4 projektet és a View engine-t ASPX-re állítjuk, akkor a Shared
mappában egy Site.Master nevű fájlt fogunk találni, tehát a Web Forms view
engine-nél master maradt a név.
Ha még emlékszünk, a View mappák című részben felülbíráltuk a RazorViewEngine működését, hogy
ne keresgéljen annyit. Ez a master-layout névváltás ennek a CreateView metódusában történik meg.
6.6 A View - A View nyelvezete 1-139
Az eddigi oldalakon láttunk számos View példakódot is, azonban a részletekbe nem merültünk el. Itt
az ideje megnézni a View-ba megfogalmazható kód + markup szintaxisát.
Nagyszerűsége abban rejlik, hogy nem kell a rövid kód darabkák végét jelölni. Márpedig ha elkezdjük
tömegével írni a View-kat, a legtöbb sorban csak pár szavas kód is elegendő lesz ahhoz, hogy kifejtsük,
amit szeretnénk. Kényelmetlen és zavaró (lehet) az ASP.NET Web Forms szintaxisánál, hogy a kódot
mindig közre kell zárni. Ráadásul olyan esetben is, amikor a kód nem más, mint egy előzően megnyitott
if kódblokkot lezáró kapcsos zárójel (egy darab hasznos jel miatt 5-öt kell leírnom <%}%>)
Az ilyen „sokat gépelős” fejlesztést más platformokon sem szeretik. Ott van pl. a PHP, ahol a kódot
szintén markerek közé kell tenni (<?php ?>). Mivel ez még hosszabb, már régen rájöttek, hogy ez
fárasztó az ujjnak és a szemnek is, ezért kialakították a template nyelveket. (pl. Smarty) Az ilyen
template nyelvek közös jellemzője.
Az MVC-ben a 3.0 változattól vált elérhetővé egy ilyen template nyelv: a Razor. A példák kódjai egyben
a Views/Razor/Szintaxis.cshtml fájlban találhatóak. Nézzük végig példákon, hogy milyen lehetőségeket
biztosít a razor.
Változó beágyazva <b> elemek közé. A Title szót azonnal követheti a </b> nem kell space:
Viszont az egysoros razor esetén nem lehet a C# utasításban space. (A C# normál kódban megengedi a
space-t a műveleten belül: ViewData [ "1" ] = "A"; ). A harmadik „Oldalcím” végén ott van a
pontosvessző, pedig nem szabad kiírni a megszokott C# utasítás lezárást, mert a pontosvesszőt is
belerakja a HTML markupba.
A <span> -ra azért volt szükség, mert azt nem írhattam, hogy „@Request.Browser.Browser.Nem
létező property”. A „Nem” szót property elérésnek vette volna. A "Nem" property nem létezik (A
Browser property egy string).
Teljesen beékelt változót megjelenítő razor szakasz az <a> tag-be ékelve az idézőjelek közé. (A link az
aktuális oldalra visz vissza…)
Előfordul, hogy összetett műveletet szeretnénk elvégezni, vagy space-eket akarunk tenni az egysoros
kódba, ezt normál zárójellel tehetjük ezt meg. Az előbb látott (Oldalcím4), nem létező property
problémát is meg lehet ezzel oldani. A @(…) zárójelekkel egyértelműen jelezhetjük a kód határát. A
közrezárt kódnak szükséges, hogy legyen olyan visszatérési értéke, amit HTML kimenetté lehet
alakítani.
Az első sor mutatja, ha email címet akarunk kiíratni, amibe a "@" jel is kell (egyébként ne tegyünk soha
így). Egy kukac megjelenítéséhez írjunk kettőt.
A több sornyi C# kódot kódblokkba tudjuk tenni, amit { } közé kell írni.
@{
//Kódblokk C# nyelven
Write("Közvetlenül írok a kimenetre<br />");
WriteLiteral("<br /><em>Közvetlenül írok a kimenetre HTML kódot is<em/><br />");
}
<br />
A Write metódus szöveget vár, amit viszont okosan HTML-é kódolja, emiatt a szövegbe írt „<br>” nem
HTML-ként kerül a böngészőhöz. Ezért van a sor végén olvasható formában. A WriteLiteral nyersen írja
ki a kimenetre a paraméterben kapott stringet.
Amikor kódblokkon belül nem HTML tagek közé zárt szöveget írnánk ki, akkor gondban leszünk, mert
az nem értelmezhető C# kódként. Ezt egy sor esetén a „@:” markerrel tehetjük meg. Több sort a <text>
tag-el vonhatunk ki a C# fordítás alól.
Hasznos, hogy a razor fordító a HTML tagek közé írt szöveget, a tageket is beleértve, nem értelmezi
kódnak. <mindegy>ez nem C# kód</mindegy>.
A szövegbe tudunk kódot beékelni, mindegy, hogy HTML vagy normál szöveg (ld. Guid-os sorok).
Érdemes megfigyelni, hogy a NewGuid() metódushívás utáni lezáró zárójelet szövegesen fogja
megjeleníteni, hasonlóan a @Guid előttihez.
@if (1 == 1)
6.6 A View - A View nyelvezete 1-141
{
//Kódblokk feloldása
@:Kódblokkon belüli, egysoros, nem html szöveg
<p>Kódblokkon belüli, html szöveg -> nem kell feloldás</p>
@:Nem html szöveg kóddal (@Guid.NewGuid())
<br />
<b>Html kóddal (@Guid.NewGuid()) </b><br />
<text>
Kódblokkon belüli, több soros, nem html
szöveg
feloldása
.
.
</text>
<br />
@:{ Kapcsos zárójelek között }
}
A vezérlési szerkezeteknél (if, else, for, while, foreach) a C# fordító megengedi, hogyha azt csak
egysoros utasítás követi, akkor a nyitó és záró kapcsos zárójeleket elhagyjuk és elég az egészet egy
darab pontosvesszővel lezárni.
Ez nem működik a razor esetén, ezért kénytelenek vagyunk a kapcsos zárójeleket kiírni még az alábbi
egy kulcsszavas kód esetén is:
Az olyan esetben, amikor csak két érték közül kell választani, az if+else szerkezet helyett használhatjuk
a '?' operátort is:
Természetesen ilyenkor nem használhatunk csak egy @-ot mindenképpen a @( ) formulát kell igénybe
venni.
Az MVC 4 újdonsága, hogy ha egy property értékét egy HTML tag attribútumába írjuk, akkor azt
értelmesen kezeli. A következő példában a ViewBag.CssOsztaly-nak szándékosan nem adtam értéket,
tehát az null.
A div1 id-jű div class definíciója a HTML kódban az lesz, hogy <div class=”foosztaly” id=”div1”> és nem
„foosztaly null”
A div2 class és mindegynevu definíciója el fog tűnni, mindössze ez marad: <div id=”Div2”>. Úgy
gondolja, hogy az üres attribútumnak úgysem lesz haszna, ezért az attribútumot is törli. A 14. példakód
mutatja a HTML eredményét.
A modell tárgyalásánál említettem, hogy a viselkedéssel bővített modell hasznos tud lenni azzal, hogy
a modell belső állapotát propertykkel (számított értékekkel) jelezni tudjuk a View számára. Ha a
property nem csinál mást, mint egy CSS osztály nevet vagy null-t ad vissza, akkor azt közvetlenül be
tudjuk injektálni a HTML attribútumba és nincs szükség if-re vagy ? operátorra.
6.6 A View - A View nyelvezete 1-142
Van egy kivétel. Ha a HTML attribútum „data-”–val kezdődik, az üres attribútumot nem törli, mert ezzel
az előtaggal kezdődő attribútumok tartalom nélkül is jelentéshordozók lehetnek a HTML 5
értelmezésében.
@{ViewBag.isChecked = true;}
Egy kis ínyencség következik. Vajon mi lesz ennek az input definíciónak a renderelt HTML eredménye?
Ilyen definíciót azért elég ritkán kell készíteni, de az érem másik oldala, hogy nem pontosan azt kapjuk,
amit várnánk.
@*Kommentezett sorok
...*@
Gyakran előfordul, hogy a javascript kódba megjegyzést teszünk. Hacsak nem az a célunk, hogy a
böngésző forrás nézetében is olvassák a megjegyzésünket, akkor a JS megjegyzéseit a második sor
(variable2) razor komment módjával érdemes írni. Ha már a .cshtml fájlba írtuk a <script> blokkot talán
így jobb.
<script type="text/javascript">
var variable1 = 'abc'; //abc -> variable1-be
var variable2 = 'abc'; @*abc -> variable2-be*@
</script>
6.6 A View - A View nyelvezete 1-143
Az előző sorok egyesített eredménye, alatta a generált HTML kód vége a div1 -től:
<div id="Div2">
Div2
</div>
<script type="text/javascript">
var variable1 = 'abc'; //abc -> variable1-be
var variable2 = 'abc';
</script>
14. példakód
A Razor nem csak egy szintaxis, hanem vannak speciális funkciói is. A @model, @section funkciókkal
már találkoztunk. Mielőtt folytatnánk, meg kell nézni a View lelkivilágát is. Most egy kis mélyvíz és
utána újra egy kis Razor ismertető (pihentető) következik.
Ha a kérdés az, hogy milyen adatokat tudunk elérni a View-ban levő kódból, a rövid válasz, hogy
mindent, ami az oldal eddigi életciklusában elérhetővé vált. Ahogy a kontroller adat kontextusánál már
láttuk, itt is számos property van kivezetve a View példányra. Álljunk is meg egy pillanatra. Mi az, hogy
View példány? A View-ról eddig azt mondtam, hogy ez egy HTML oldal, dinamikus és statikus
tartalomszakaszokkal. Ezt nem lehet példányosítani, mert csak osztályt lehet. Láthattuk, hogy a View
belső szerkezete minden csak nem osztály definíció. Ennek ellentmondva azt tapasztalhatjuk, hogy a
View osztály példánya az objectből származik. Ezért van neki GetType() metódusa is:
@{
Type viewTipusa = this.GetType();
}
Jöjjön a magyarázat: Az action metódusból kilépve egy ViewResult-ot adunk vissza a View() metódus
meghívásának eredményével, vagy közvetlenül példányosítunk egy ViewResult-ot. Ez, mint egy
alkatrész csomag átkerül az MVC-hez, amiből az meghatározza a View fájlt és ezt a fájlt osztállyá
transzformálja. Utána az osztályt pedig le is fordítja CIL kóddá és beleteszi egy assembly (dll) fájlba. Ez
elég meleg nem? Nagyon hatékony módszer, mivel a View-t csak egyszer kell „lefordítani” utána a kész
dll fájl tárolható és használható. A legközelebbi request érkezésekor már készen áll egy mini assembly.
Az ebben levő osztályt csak példányosítani kell és indítani. Ez a magyarázata annak, hogy a View első
felhasználása (indítása) miért sokkal lassabb, mint a rákövetkezők. A View szöveges fájljának állandó
újraértelmezése rendkívül erőforrás-igényes lenne. Járjunk egy kicsit utána! Az action legyen egy
lényegtelenül egyszerű kód:
<h2>ViewContext felfedése</h2>
A View típusa: @viewTipusa <br />
A View dll fájlja: @viewTipusa.Assembly.Location
A lefordított assembly-t, ezen az irgalmatlan elérési úton lehetett megtalálni. Gondolom nyilvánvaló,
hogy a mappák és fájl nevének kódolt neve dinamikusan van meghatározva, tehát az MVC
alkalmazást egy másik gépen futtatva biztosan más lesz a fájl és az elérési út is. Az assemblyben nem
csak az aktuális View kódja van, hanem az azonos mappán belüli többi View is belekerül. Ez egy
előtakarékosság. Az assembly fájl nevével egyezően ebben a mappában még ott vannak a C#
forráskódok is. Megkerestem a konkrét View forrását, és kigereblyéztem belőle, ami számunkra most
nem fontos. A lényege úgy is az, hogy a View-ba írt szöveg string formában kerül kiíratásra. A View
elején levő típusinformációkat kiíró kód pedig úgy van ott, az Execute metódusban, ahogy azt
megírtam.
Természetesen a kommentek sem voltak az eredeti kódban. Az osztály neve, némi kiegészítéssel a
View cshtml fájl elérési útjából tevődik össze: Views/ViewTest.cshtml
Érdekességként itt a _Layout.cshtml C#-ra fordított kódjából egy részlet (//… kivett szakaszok):
Megjegyzendő, hogy az általunk írt hagyományos kód az Execute() metódusba kerül bele. Így normál
esetben csak olyan kódot írhatunk a View-ba, ami egy C# metóduson belül is megállja a helyét.
Osztály-, property- vagy metódusdefiníciót nem. A nem normális esetet a @helper és a @functions
kulcsszavakkal bevezetett blokkal tudjuk megvalósítani (a következő részben megnézzük…).
A fentiekből következik, hogy a View futásából is ki lehet "ugrani". Elég gyakran alkalmazom a
következő if-es vezérlési szerkezetformát, hogy a felesleges {{{ … }}} jellegű mély beágyazásokat
elkerüljem, és kicsit rövidebb és olvashatóbb legyen a kód:
Ugyan ez a módszer működik a View kódjában is. Bárhol, ahol a View további tartalmának az
előállítása lehetetlen vagy értelmetlen lenne (egy hiányzó objektum miatt) az @if(valami !=
null) { return; } formulával megszakíthatjuk a futását. Ez félkész View tesztelésénél,
hibakeresésnél is jól jöhet.
Amikor a Visual Studio-ban egy MVC projektet lefordítunk, akkor a View-ban levő kódot nem fogja
bevonni a fordítási procedúrába, mivel arra majd az „élő műsorban” kerül sor. Tehát, ha hibát írtunk
a View-ba, az is csak futásidőben fog kiderülni. Ez bosszantó tud lenni, amikor az éles alkalmazást
szeretnénk telepíteni és biztosak akarunk lenni, hogy legalább fordítási hiba nem fog megtörténni
később a felhasználó szeme láttára. Egy kis trükkel rávehetjük a VS-t, hogy mégis fordítsa le nekünk a
View-kat is. Az MVC projektfájlt kell szerkeszteni hozzá. Ezt futó VS mellett két módon is
megtehetjük.
6.6 A View - A View nyelvezete 1-146
Térjünk vissza a Razor nyelvi lehetőségeihez. A szekciók és ezek beékelését megvalósító. @section-t
már részletesen megismertük a _Layout tárgyalásánál. Ennek nincs C# kóddal kapcsolatos funkciója,
csak szekciókat injektál a View-ból a hosztoló Layout-ba.
Szintén láttuk már a @model –t, amivel a View által igényelt és használt modell típusát tudjuk
deklarálni. Ezzel típusossá tudjuk tenni a View-t. Ennek a legnagyobb haszna, hogy a View Model
propertyt ilyen típusként tudjuk elérni, míg ha nem használjuk a @model-t, akkor a Model property
dinamikus (dynamic) típusú lesz. Amiben az a legrosszabb, hogy nincs IntelliSense támogatása. A View
osztálytípusa a @model által meghatározott generikus leszármazott lesz.
Azt mondtam, hogy @model típusossá teszi a View-t. A View mellett típusosak lesznek a ViewData, a
Html és Ajax helperek szintén. Emiatt tudjuk használni többek között a Html.TextBoxFor,
Html.LabelFor, Html.EditorFor és az összes …For –végű Html helpert.
Jöjjenek az újdonságok. Nagyon valószínű, hogy egyszer a View kódunkban hosszabb névterű típust is
szeretnénk használni. A C# kód írásakor megszokott using itt is használható például
@using System.Web.Http formában, valahol a View fájl elején. A lezáró pontosvesszőre itt sincs
szükség.
6.6 A View - A View nyelvezete 1-147
A @using –ot nem érdemes használni abban az esetben, ha a névteret szinte az összes View-ban
használjuk. Ugyanis a Views mappa alatt található web.config fájlban van egy szakasz, ahová további,
közösen használt névtereket vehetünk fel:
<namespaces>
<add namespace="System.Web.Mvc" />
<add namespace="System.Web.Mvc.Ajax" />
<add namespace="System.Web.Mvc.Html" />
<add namespace="System.Web.Optimization"/>
<add namespace="System.Web.Routing" />
<add namespace="A.Közösen.Használt.Névtér.Helye" />
</namespaces>
A Html helperek HtmlAttributes paraméterével tudunk HTML tag attribútumokat deklarálni, egy
anonymous típuson keresztül.
A HTML tagek közül a legfontosabbal a „class”-al gondban lennénk, mert ez egy C# kulcsszó (szintén a
legfontosabb). A dilemma feloldásához a C#-hoz hasonlóan, a razorban is használhatjuk a @class
formát. Ennek funkciója tényleg csak az, hogy a class szót bele tudjuk írni egy anonymous típus
property listájába. A használatára a sor végén látható a példa.
A kód létrehoz egy linket a Szintaxtis oldalra. Az <a> tag class értéke „kiemelt” lesz. <a
class="kiemelt" href="/Razor/Szintaxis" id="ug1">Ugrás a szintaxis oldalra</a>
Szintén gondban leszük, ha az attribútum neve kötöjelet tartalmaz. Ez pedig nagyon fontos egy HTML
5-ös oldal létrehozásához a sok data-* formátumú attribútum esetében. A C# osztály-, változó- vagy
propertynév nem tartalmazhat kötőjelet, mert kivonást jelent. (int data-list = 1;)
A probléma megoldása, hogy aláhúzást írunk a property nevében:
new {data_html5 = "", data_value=""}. Ezt érteni fogja a feldolgozás során, és átalakítja kötőjellé
az aláhúzásokat, és lesz belőlük data-html5 és data-value HTML attribútum.
Ebből következik, hogy aláhúzást tartalmazó HTML attribútumot így nem tudunk definiálni. Erre az
esetre marad a hagyományos szótárfeltöltős definíció:
A háttérben az anonymous objektumból minden esetben ilyen szótárat készit az MVC. Ez a biztosabb
út, habár kicsit többet kell gépelni. Az inline template megvalósítására eddig csak utaltam. A kulcsszó
a @helper
<h2>Inline Template</h2>
<ul> @for (int i = 1; i < 5; i++) {
@ListItemTemplate(i)
}
</ul>
6.6 A View - A View nyelvezete 1-148
A működés nagyon egyszerű a @helper kulcsszóval deklarálunk egy normál metódust, aminek a belseje
egy razor kódblokk. Visszatérési típust nem tudunk megadni (egyébként HelperResult). Rakhatunk bele
kódot és HTML markupot egyaránt, ahogy egy razor blokk megengedi. Majd bárhol az adott View-n
belül úgy használhatjuk, mint egy metódust, ahova belekerül a @helper-ben megírt sablon (template)
renderelt eredménye.
Nagyon hasonlót lehet elérni az un. Razor delegate-el, ami inkább egy trükk.
@{
Func<dynamic, object> hangsulyos = @<em>@item</em>;
}
@hangsulyos("Hangsúlyos szöveg")
Úgy is dönthetünk minden ajánlásom ellenére, hogy telerakjuk a View-t üzleti kódokkal. Ennek
támogatására született a @functions kódblokk.
@{
var ic = new InsideClass("Kód a View-ban");
}
@functions
{
int numberTen = 10;
string txt = "Kerüld el, ha teheted!";
A @functions blokkban megírt kód úgy fog megjelenni a View lefordított kódjában, mintha oda írtuk
volna a View forrásába kézzel. A txt osztályszintű változó lesz, az InsideClass pedig a View osztályán
belül deklarált osztály. A GetCssClass metódus a View osztályának a metódusa lesz. A fenti kódból
kibogarászható egy apróság, amit nem említettem a razor szintaxisnál. A @MetodusNev() formula
használata megköveteli, hogy a metódusnak legyen visszatérési értéke, ami célszerűen MvcHtmlString.
A fenti View-ból generált kód lényegi része sokat elárul a razor funkciók hatásáról:
6.6 A View - A View nyelvezete 1-149
WriteLiteral("\r\n");
Write(hangsulyos("Hangsúlyos szöveg"));
WriteLiteral("\r\n\r\n<br />\r\n");
var ic = new InsideClass("Kód a View-ban");
WriteLiteral("<br />\r\n<div");
WriteAttribute("class", Tuple.Create(" class=\"", 505), Tuple.Create("\"", 527)
, Tuple.Create(Tuple.Create("", 513), Tuple.Create<System.Object, System.Int32>(GetCssClass()
, 513), false)
);
WriteLiteral("></div>\r\n\r\n");
WriteLiteral("\r\n");
Write(GetType().Assembly.Location);
}
}
A View-ból egy osztály lesz és a létrejövő kód osztálya a WebViewPage<TModel> leszármazottja lesz.
egy új típust. Ezek után az új típust használja a View őseként és nem a WebViewPage-et. Ezzel az előbbi
@functions-ba írható kódot tudjuk egységesíteni több View számára is.
Ugye, hogy milyen nyitott rendszer ez? Ilyen az, amikor egy framework-öt kreatív arcok fejlesztenek.
Context (HttpContext)
o Cache
o Request
o Response
o Server
o Session
o User
o Application
o …
TempData
ViewData
ViewBag
Model
Layout
Sajnos a HttpContext a „Context” nevű propertyn érhető el, pedig a Controller osztálynál
„HttpContext” volt a property neve.
A kontextusból kinyerhető adatokat a View kódjában tudjuk felhasználni. Ilyen volt az egyik korábbi
példában a @Request.Url változó megjelenítése. Ezeken felül még néhány érdekes tulajdonságról
tennék említést:
DisplayMode – Az MVC4-ben jelentek meg a display mode-ok, amik alapján különböző fájlnevű View
variánsokat tudunk használni, attól függően, hogy normál számítógép vagy mobil eszköz
böngészőjével látogatták meg az oldalunkat. Erről a 11.3 fejezetben még részletesen szó lesz.
Továbbá elérhető néhány hasznos metódus is, a Html, Ajax helperek típusos példányán kívül is.
IsSectionDefined(string name) – A name nevű section definiálva van-e a beágyazási láncban aktuális
View fölött. Normál View esetén például a _layout.cshtml-ben.
RenderPage(string path, params object[] data) – A megadott elérési úton levő partial View futási
eredményét illeszti a megadott helyre.
6.8 A View - Beépített Html helperek 1-151
DefineSection("scripts", () => {
WriteLiteral("\r\n<script type=\"text/javascript\">\r\n function showAlert(){" +
"alert(\'Ez a scripts section\');}\r\n" +
"</script>\r\n");
});
Write(object value) – Közvetlen írás a kimenetre, de mint már láttuk ez nem engedi át a HTML tageket,
hanem entitásokkal helyettesíti
WriteLiteral(object value) – Szintén közvetlen írás a kimentre, de nyersen. Ezzel lehet HTML tageket
is kiíratni.
Remélem iránymutatónak elég lesz ennyi is a View kontextusából. Szinte minden itt van, csak a
kontroller adatai nincsenek, mert azok elhaláloztak időközben.
A View HTML sablonként funkcionál. Amit beleírunk, az megjelenik majd a böngészőben. Ezzel el is
intéztem ezt a fejezetet, átugorhatunk a következőre. Az MVC készítői nem voltak ennyire kegyetlenek,
mint én, és rendelkezésünkre bocsájtottak jó néhány olyan metódust, amivel a <b> tagnél
bonyolultabb HTML elemek hatékony és típusos írásához nyújtanak segítséget. A segítségnyújtás
része, hogy ezek a metódusok hidat képeznek az MVC infrastruktúrája, logikája, modellje és a HTML 4
nyelvi szintaxisa közé. Nem véletlenül írtam 4-es verziót. Egyelőre, a HTML 5 új lehetőségeit részben
használják csak ki a beépített helperek. Láttuk, hogy a DataType attribútum alapján a célnak megfelelő
típusú input mező kerül a generált kódba, de ennél sokkal messzebb nem merészkedik a generálás.
Az egyik csoport a bemenő paraméterek alapján, összeállítja a komplett HTML elemet. A paraméterek
a HTML elem lényeges attribútumait töltik fel.
A másik csoportba tartozók számára csak annyit adunk meg, hogy a modell melyik propertyje alapján
dolgozzon, a többit találja ki maga a metainformációk alapján. Ez utóbbiak, a modellhez kapcsolt Html
helper metódusok és nevükben rendre tartalmazzák a „For” szócskát. Mindenképpen várnak egy
lambda expression-t paraméterként, ami azt a propertyt határozza meg, aminek az értékét kell
megjeleníteni a Html markupban. Ezeket a helperváltozatokat csak típusos View-ban tudjuk használni,
ahol a @model –el meghatároztuk a modell típusát.
6.8 A View - Beépített Html helperek 1-152
A View kódján belül elérjük a Html propertyt. Ez egy generikus HtmlHelper példányt hordoz, ami ha
nem adtunk meg modell típust a View számára (a @model-el) akkor dynamic (HtmlHelper<dynamic>),
ha megadtunk, akkor a modellünk típusa lesz a generikus paraméter (HtmlHelper<aModelTipusa>). A
HtmlHelper osztály magában hordoz egy sereg metódust, amik a HTML markup előállításában
segítségünkre lesznek. Itt jellemzően Html linkek, attribútumok, Id-k előállítására, adatformázásra, stb.
használható metódusokat találunk. A HtmlHelper a System.Web.Mvc névtérben található, ezt csak
azért emelem ki, mert van egy másik névtér a System.Web.Mvc.Html, ami tartalmazza a legfontosabb
HTML elemek előállítását végző bővítő metódusokat (extension method) és még sok hasznos dolgot.
Raw
Kezdjük a legegyszerűbbel, amikor a paraméter egy string, aminek az értékét az adott helyen
szeretnénk viszontlátni az elkészülendő HTML fájlban. A "féktelen nyers erő" Html.Raw metódussal
tehetjük ezt meg. Azért féktelen, mert a bejövő szöveget gondolkodás és minden ellenőrzés nélkül
küldi a HTML-be. Ezt is:
Html.Raw(„<script>alert(’Lopom a cookie-dat!’)</script>”)
Az oldal eredménye a javascript jól ismert dialógusablaka lesz. Persze a JS kód lehetne veszélyes is. A
szöveg is erre utal. Például a site-hoz tartozó cookie tartalmát továbbíthatja egy másik web
applikációnak, ahol majd elcsemegéznek rajta. Lehet, hogy még pénzt is tudnak csinálni belőle a mi
rovásunkra. A rébuszok után egyenesen is megmondom, hogy ezt a metódust nagyon óvatosan
használjuk. Számtalan módszer van arra, olyan is amiről nem hallottunk, hogy az oldalunkra
bejelentkezett felhasználótól származó nyers szövegbe, ártalmas kódot csempésszenek. Ha ezt
egyszerűen kiküldjük az oldalunkra, azzal a rendszerünket és a felhasználóink adatait is veszélyeztetjük.
Az MVC további Html helperei védettek az ilyen nyers adatkiküldéssel szemben. A bejövő szöveges
tartalom HTML markereit (< >) átalakítják html entitásokká29. Pl.:
< <
> >
& &
Ezzel a JS kód injektálásnak is az elejét veszik. A @változó razor szintaxissal, amivel egy változó, vagy
tulajdonság értékét érjük el, szintén nem tudunk nyers HTML szöveget kiíratni.
29
http://www.w3schools.com/html/html_entities.asp
6.8 A View - Beépített Html helperek 1-153
Hidden és HiddenFor
A normál HTML <input type="hidden" value="…."> formájú mezőt lehet generálni vele. Ha a
kapcsolódó property rendelkezik a [HiddenInput(DisplayValue=true)] attribútummal és bekapcsolt
DisplayValue-al, akkor a hidden mező előtt még szövegesen is megjelenik a tartalma. Egy
különlegessége, hogyha a property típusa byte[] vagy System.Data.Linq.Binary, akkor annak a
tartalmát Base64 kódolású szöveggé alakítva használja a value értékeként.
ActionLink
Ezt is használtuk korábban. Jöhetnek a részletek: Az első példában megadjuk az <a> tag URL-je helyett
az action metódusunk nevét (Hlink2) és a link szövegét. Alatta a keletkezett HTML sor, amiben látszik,
hogy az action neve és az aktuális kontroller nevéből képzi a href értékét.
@Html.ActionLink("A 2. oldalra, URL paraméterrel", "Hlink2", "Helper", new { honnan = "Hlink" }, null)
Ebben a példában az URL paramétereket collection initcializer-el állítjuk be. Az eredmény azonos lesz.
Lehetőségünk van HTML attribútumok meghatározására is anonymous objektummal, ami egy elegáns
kódformát ad.
RouteLink
Nagyjából mindent meg lehet oldani az ActionLink-el is, de van még egy linkgenerálási lehetőség a
RouteLink. Ez nagyobb site-oknál ad segítséget és lehetőséget arra, hogy több route bejegyzés közül
kiválaszthassunk egyet a route map neve alapján. Ha sok route bejegyzésünk van, előfordulhat, hogy
a kontroller és az action név meghatározása nem elég, mert más bejegyzésre is ráillene, ami megelőzi
a kívánt sort a route listában. (a lista első illeszkedő eleme nyer ld.: 5.3 Routing fejezet). Az itt
következő első példának sok hasznát nem vesszük, mert teljesen úgy fog működni, mint egy ActionLink.
routes.MapRoute(
name: "complains",
url: "complains/{controller}/{action}/{id}",
defaults: new { id = UrlParameter.Optional }
);
Látható, hogy az URL a „complains” route bejegyzés url paramétere szerint állt össze. A RouteLink
sajátossága, hogy az URL-t értelmesen állítja össze. Tehát ha a megadott paraméterek szerint nem
képezhető link, akkor az alkalmazás gyökeréhez készíti a linket.
Rendelkezésre áll a HTML form generálásához szükséges Html.BeginForm metódus. AZ ismertető előtt
szeretnék mutatni a <form> tag használatáról néhány szempontot azoknak, akik ASP.NET Web Forms
alkalmazásokat fejlesztettek eddig. Érdemes elmélkedni a HTML form lehetőségein, mivel a Web
Forms az oldalak interakcióinak kezeléséhez kisajátítja azt magának. Tehát egy kis rehabilitációs
elmélkedés, vagy egy kis ismétlés .
A HTML form két fontos paraméterrel rendelkezik. Az egyik az action, ami egy URL vagy csak egy URL
path, ahova a form submit műveletében (amikor megnyomjuk a submit funkciójú gombot) küldi a
<form></form> tagek közti <input> mezők tartalmát annak neveivel. Az URL lehet abszolút, azaz az
URL path eleje /-el kezdődik. (http://localhost/url/path/cont vagy /url/path/cont ). Lehet relatív, ahol
6.8 A View - Beépített Html helperek 1-155
a kiinduló pont az aktuális oldal URL-hez képest additív path. Leggyakrabban csak egy fájlnév vagy egy
szó. Példaként a GET-el lekért oldal (és benne a form) URL-je legyen: /url/path/cont és az action
attribútum mindössze csak: contback, ennek megfelelően a submitkor a /url/path/contback lesz a
böngésző által előállított URL path.
A HTML form másik paramétere a method. Ezzel tudjuk informálni a böngészőt, hogy a submit
műveletet milyen HTTP igével küldje vissza a szervernek. Ez legtöbbször a POST szokott lenni, de
mielőtt ezt kőbe véssük, elmélkedjük tovább. Lehet, hogy ez az ASP.NET-hez (és más környezethez)
szokott fejlesztő Pavlovi reflexe, de nem biztos, hogy jó minden esetre a POST. Egy nézőpontból
értelmezve a HTML formok felhasználási esetei két csoportba oszthatók:
1. A felhasználói input eredménye az, hogy a szerveren létrejön vagy megváltozik egy DB entitás.
Ezek a tipikus felhasználói űrlapok amit kitöltetünk, majd az eredményét eltároljuk. Utána a
felhasználót egy másik oldalra irányítjuk, ahol megköszönjük a vásárlást és megmutatjuk a
számla végösszegét, hadd ájuldozzon. Ez nyilvánvalóan POST method-al szokott zajlani, aminek
több oka is van. Ennél a megoldásnál valóban fontos, hogy másik oldalra irányítsuk a
böngészőt, mert ha nem, akkor a felhasználó azt fogja hinni, hogy nem jól töltötte ki az űrlapot.
A form adatok nem lesznek láthatóak, mert a HTTP csomagba lesznek benne.
2. A felhasználói input nem hoz létre és nem változtat meg semmilyen lényeges üzleti entitást az
űrlap mezői alapján, legfeljebb naplózzuk amit beírt a felhasználó a mezőkbe. Ezek a tipikus
keresési, szűrési feltételek űrlapjai. Itt megadja a felhasználó a méretet, a színt, az árkategóriát,
stb. ennek eredményeként megmutatjuk neki a szűrési feltételeknek megfelelő terméklistát.
Erre viszont nem annyira jó a POST, legfeljebb akkor, ha a szűrési feltételek nagyon sok
szempontból állnak (>10). Ha a keresési feltételek input mező adatait GET method-al küldjük
vissza, akkor a keresési oldal URL-jében megjelennek a feltételek URL paraméterek
formájában, amit a felhasználó ki tud másolni és pl. emailben tovább tud küldeni a
kollégájának, barátjának. Ezzel megkíméli őt a szűrési feltételek újra beállítgatásától, vagy akár
be tudja rakni a kedvencek közé. A felhasználók nem biztos hogy mindig úgy használnák a
rendszerünket, ahogy mi azt elképzeljük. Ilyen pici POST->GET szemléletváltás jelentős
előnnyel járhat számukra.
A HTML formból minkét paramétere elhagyható, ekkor a form action az aktuális oldal URL-je, a method
pedig a GET lesz. Arra azonban nem vennék mérget, hogy minden helyzetben pl. karórában, mikro
sütőben futó böngészőben is működni fog, ezért legalább a form actiont érdemes lesz megadni.
A következő példában az action metódus neve mellett a kontroller nevét is megadtam („Helper”), a
routeValues: id=1, a method: Post. Ott van még egy HTML attribútum csomag, az egy darab id
attribútummal (id="form1").
6.8 A View - Beépített Html helperek 1-156
A generált html:
<form action="/Helper/Hform/1" id="form1" method="post">
Szöveg: <input id="Szoveges" name="Szoveges" type="text" value="" /><br />
<input type="submit" />
</form>
Az action attribútum URL-jének a végén ott van a RouteValue id 1 értéke is. Ez egy kényelmesen
használható lehetőség, hogy a form küldése után átadjuk a form entitásának az azonosítóját.
Egyszerűbb, mint egy külön hidden mezőt fenntartani az id számára, de figyelni kell, hogy az Id-t csak
egy módon küldjük vissza. (csak hidden vagy csak route paraméter). Ha mindkét módon megadjuk, a
hidden mező értékét kapja a fogadó action metódus id paramétere, és akkor a RouteValue értéke nem
lesz figyelembe véve.
A HTML5 sok újdonságot hozott a form kezelésben ezért érdemes megadni a <form> id-t is. Az egyik
ilyen újdonsága, hogy nem kötelező a <form></form> tag-ek közé zárni az <input> és <select>
elemeket. Ezek rendelkeznek már a "form" attribútummal, amivel közölhető, hogy melyik formba
értjük bele a szóban forgó elemet (mintha ott lenne a formon belül, de a dizájn miatt nem lehet
odatenni).
A HTML űrlap önmagában mit sem ér, tehát következzenek a beviteli mezők. A kipróbáláshoz szükség
lesz két actionre. Egyre, ami a meglévő adatokkal GET esetén kiszolgálja a View-t és egy másikra, ami
a POST adatokat fogadja, amik alapján frissíti a tulajdonságokat a már megismert memória alapú
tárolónkban.
[HttpPost]
public ActionResult Hinput(int? id, FormCollection fcoll)
{
if (!id.HasValue) return RedirectToAction("Hinput");
A TextBox egy-, a TextArea többsoros szöveges beviteli mezőt biztosít. Az első paraméterük az <input>
name attribútuma lesz, a második a kezdeti szöveg értéke, ez kerül a value-ba. A keletkezett HTML sor
a TextBox alapján:
A TextArea Html helpere sem túl bonyolult. A 3. és 4. paramétere a sorok és oszlopok száma, ami el is
hagyható. (a null a HTML attribútumok definíciójának a helyét áll)
@Html.Hidden("FullNameOrig", Model.FullName)
<input id="FullNameOrig" name="FullNameOrig" type="hidden" value="Tanuló 3" />
Amikor a submittal beküldjük a formot, és az ActionResult Hinput(int? id, FormCollection fcoll) action
fogadja azt (15. példakód). A FormCollection-ban pedig ott lesznek a HTML beviteli mezők név-érték
párokban, amit most nem is használunk fel, csak a demó kedvéért van ott. Az Id-ben benne lesz az
eredeti objektum id-je, mert a form RouteValue listájába beletettük. A GetModell-el elkérjük az eredeti
entitást és a kontroller TryUpdateModel metódusával frissítjük az adatait. Valós helyzetben, ez után
következik még egy adatbázis update, de itt nincs rá szükség.
A TryUpdateModel hívásával a model binder-t aktivizáltuk, ami a háttérben a beviteli mezők neveivel
összepárosítja a modell propertyket a neveik alapján. Ha az adat érvényes, akkor felülírja a modell
propertyk adatait. A model binder egyik alapvető funkciója, hogy az action hívása előtt automatikusan
beindulva, az action metódus paramétereit be tudja állítani a HTTP post adatok alapján. Emiatt írhattuk
volna így is az action metódust:
[HttpPost]
public ActionResult Hinput2(int? id, string FullName, String Address)
{
if (!id.HasValue) return RedirectToAction("Hinput");
Ahhoz, hogy ez az action aktivizálódjon, a BeginForm-ban az action nevét át kell állítani Hinput2-re.
Az action utolsó sorában explicit megadtam, hogy a View a 'Hinput' legyen, mert a Hinput2-höz nincs
View fájl.
Ezekkel a helperekkel nincs is semmi gond, könnyítést adnak a HTML előállításához. Azonban egy
programozónak az igénye általában az, hogy ne kelljen egynél többször leírni valamit. Ebben a
szituációban azonban a modell és a View kapcsolatát manuálisan kell karbantartani. Ott van a
paraméter lista: ("FullName", Model.FullName). Kétszer is le kell írnom a FullName szót. Mi van, ha
megváltoztatnám a property nevét mondjuk TeljesNev-re, akkor mehetek végig az összes olyan View-
n, ahol használtam ezt a propertyt, mindenhol ahol szövegesen hivatkoztam rá.
Erre vannak a Html helperek „For”-os változatai. A 16. példakód form példáját le lehet írni így is:
Az eredmény közel ugyan az. A property átnevezés egyszerű, a HTML elemek name attribútuma a
property nevéből fog származni. Ezzel azonban a kényelem-rugalmasság oltárán feláldoztuk a
közvetlen ráhatás egy részét. A HiddenFor mezővel csak úgy, mint az előző form példában, ahol az lett
volna a célom, hogy a modell FullName értéke tegyen egy körutazást FullNameOrig elnevezés alatt a
GET-POST úton. Viszont a „name” attribútum meghatározása már nincs a felügyeletem alatt, a
példában a HTML attribútum manuális megadása hatástalan. Az id = ”hnev” működik, a name értéke
„FullName” marad. Erre van egy apró trükk. A HTML „name” attribútuma kisbetűs, ahogy az a
nagykönyvben meg van írva:
Csak a kimaradt Html.Password és Html.PasswordFor volna hátra, amik szinte teljesen megegyeznek a
TextBox-al. A kivétel, hogy nem kerül beállításra a value attribútum, aminek semmi értelme sem lenne.
Ezek <input type=”password” /> jelszó beviteli mezőt hoznak létre a szokásos pöttyökkel.
6.8 A View - Beépített Html helperek 1-159
Ami nem tetszik az előző példában továbbra sem, hogy egy @:Szöveg –el adtam tájékoztatást a
felhasználó számára, hogy mit is írjon a mezőbe, holott rendelkezésre áll a propertyhez tartozó Display
attribútumban a felirat. A HTML <label>-nek a for attribútuma adja meg, hogy melyik beviteli mezőhöz
tartozik logikailag. Ezzel a kapcsolattal el lehet érni többek között azt is, hogy egy checkbox-al
összekapcsolva a label-re kattintva is lehet a checkbox állapotát változtatni. Íme, a demó View lényeges
tartalma:
Label: @Html.Label("FullName")
<br />
LabelFor: @Html.LabelFor(m=>m.FullName)
<br />
LabelFor: @Html.LabelFor(m=>m.FullName,"Teljes név")
<br />
Value: @Html.Value("FullName","Teljes név: -{0}-")
<br />
ValueFor: @Html.ValueFor(m=>m.FullName,"Teljes név: *{0}*")
A Html.LabelFor pedig a szokásos lambda expression-t várja a property szöveges neve helyett. Valós
életben előfordul, hogy úgy jönnek össze a modellek, hogy a modell propertyn definiált Display
meghatározását finomítani kell egyes esetekben. (Pl. mert nagyon hasonló feliratok jönnének össze
azonos formon belül). Ekkor lehet használatba venni a Label és LabelFor második paraméterét, amivel
felülbírálhatjuk a <label> feliratát. (@Html.LabelFor(m=>m.FullName,"Teljes név")). Ettől
függetlenül a HTML label for attribútumának értéke helyesen a property nevéből fog képződni.
Ahogy a példában is látszik a Html.Value leginkább egy alternatív mód, ha a kimenetet formázni
szeretnénk. A string.Format –ban megadható formázás szerint lehet kialakítani a kimeneti eredményt.
Van még egy label jellegű helper a DisplayNameFor, amivel a propertyre ragasztott Display attribútum
által meghatározott feliratot tudjuk elérni. Ha nem használjuk a DisplayAttribute-ot, akkor a property
neve fog megjelenni.
6.8 A View - Beépített Html helperek 1-160
DropDownList
Jól ismert lehetőség, mikor egy lista elemei közül lehet választani a HTML <select> elem
felhasználásával. A <select> elem egy <option> felsorolást zár közre. A listában kiválasztott option
elemet selected=”selected”–el jelöljük. A DropDownList nem típusos változat használatát az előzőek
alapján már nem részletezném. Helyette nézzük meg a modell típusos DropDownListFor változatát az
alábbi példán keresztül:
@using MvcApplication1.Models
@model ActionDemoModel
Mivel ez az eddigi legösszetettebb helper, részletesebb magyarázatot igényel. A form definíció már
ismerős. Ez hordozza a modell példány Id-jét és meg van adva az id attribútum is. A Html.LabelFor a
feliratért felel. A számunkra lényeges elem a DropDownListFor első paraméterében expressionben
várja a propertyt, ami számára az értéket fogja beállítani. A következő paraméter az <option> elemek
forrása: a SelectList. Ennek legalább három paramétere használatos.
2. egy property név, ami a lista elemének az azonosítója (kulcsa, key), ez kerül majd beállításra a
DropDownListFor első paraméterében meghatározott propertybe, annak az option elem alapján amit
kiválasztottak. Szóval ez lesz a kiválasztott érték.
3. még egy property név, ami a lista elemének a megjelenítendő szöveges property neve. Ebből lesznek
a legördülő listának a látható elemei.
4. Ezzel a nem kötelező paraméterrel megadhatjuk azt az értéket, amelyiket eleve kiválasztottként
szeretnénk megjeleníteni. Ennek a típusának meg kell egyeznie a 2. paraméterben megadott nevű
property típusával.
[HttpGet]
public ActionResult Hcombo(int? id)
{
return View(ActionDemoModel.GetModell(id ?? 1));
}
[HttpPost]
public ActionResult Hcombo(ActionDemoModel inputmodel)
{
var model = ActionDemoModel.GetModell(inputmodel.Id);
if (inputmodel.KeyPurchase.Id != model.KeyPurchase.Id)
{
model.KeyPurchase = model.PurchasesList
.FirstOrDefault(v => v.Id == inputmodel.KeyPurchase.Id);
6.8 A View - Beépített Html helperek 1-161
}
return View(model);
}
A get-re reagáló actionben a megadott id alapján a modellt továbbítjuk a View-nak. A post action
paraméterének típusa a modellünk típusa. Mivel nekünk a formon mindössze egy db input elemünk
van, amit a KeyPurchase.Id propertyvel kapcsoltunk össze, ezért csak ez az egy érték lesz az
inputmodel-ben kitöltve. Ez a mi esetünkben még string->int típuskonverzión is át fog esni. (a HTML
<option> value attribútuma természetesen string). A generált HTML markupból kiemeltem a <select>
elemet, azt is egyszerűsítve:
Amire a figyelmet szeretném felhívni, hogy a name attribútum egy property bejárást tartalmaz. Mivel
a KeyPurchase nem alaptípus, így ezzel nem tudna dolgozni a dropdownlist. Emiatt ennek az Id
azonosítóját kellett megadni. A model binder ezt a formulát is jól kezeli.
A post feldolgozásért felelős actionnek már csak az a dolga, hogy elővegye az id által meghatározott
eredeti objektumot. Ezután „csináld magad” módszerrel be lett állítva a KeyPurchase teljes objektum
a kapott inputmodel.KeyPurchase.Id alapján.
Természetesen nem csak a SelectList segítségével lehet az elemek listáját megadni, hanem közvetlenül
SelectListItem objektumok gyűjteményével is. Ez a típus egy nagyon egyszerű hordozó osztály, három
propertyvel:
A SelectList osztály sem csinál mást, csak egy SelectedListItem elemű gyűjteményt hoz létre és feltölti
azok értékeit.
Álljon itt még egy listafeltöltési lehetőség, (amire az MVC forráskódjában találtam rá), előtte soha nem
olvastam róla. A View-ban úgy adtam meg a dropdownlist-et, hogy a listaelemek forrása null:
[HttpGet]
public ActionResult Hcombo1(int? id)
{
var model =ActionDemoModel.GetModell(id ?? 1);
ViewData["KeyPurchase.Id"] = new List<SelectListItem>()
{
new SelectListItem() {Selected = false, Text = "Alma", Value = "1"},
new SelectListItem() {Selected = true, Text = "Körte", Value = "2"},
new SelectListItem() {Selected = false, Text = "Szilva",Value = "3"},
};
return View(model);
}
A mostani példában ez nem jött ki, de az esetek legnagyobb részében a legördülő lista sorait valamilyen
közös törzsadat elemeivel töltjük fel, mit pl. kategóriák, csoportok, típusok, állapotok. Ezek pedig
nagyon ritkán változnak, ezért a forrás lehet egy singletonban vagy egy cache-ben tárolt lista is.
ListBox
A legördülő listának a sajátossága, hogy csak egy elem választható ki. Létezik még a Html.ListBoxFor
helper is, amivel olyan listát készíthetünk, amelyben több elemet is kiválaszthatunk. Ennél
természetesen a feltöltendő propertynek is meg kell valósítania az IEnumerable interfészt, ahol
tárolhatjuk a kiválasztott elemek value értékeit. Ez legegyszerűbb esetben egy tömb. A model
kiegészítése:
A View szakasz:
Az action:
[HttpPost]
public ActionResult Hlist(ActionDemoModel inputmodel)
{
var model = ActionDemoModel.GetModell(inputmodel.Id);
model.KeyPurchaseIds = inputmodel.KeyPurchaseIds;
//return RedirectToAction("Hcombo", new { id = model.Id });
return View("Hcombo", model);
}
6.8 A View - Beépített Html helperek 1-163
A példakódban a View fájl azonos, így a teljes kinézet ilyenre sikerült. Mindkét
esetben a BeginForm("Hcombo", null, …), BeginForm("Hlist", null, …) első
paramétere adta az actiont. A controller paraméter értéke null, így ez az aktuális
kontroller lesz. Ha itt az utolsó Hlist action végén nem a View metódust, hanem
a RedirectAction-t használtam volna, akkor az URL nem változott volna attól
függően, hogy melyik „Ment” gombot nyomtam meg. Felső esetén az URL:
/Helper/Hcombo/1-re, az alsó esetén a /Helper/Hlist/1-re váltott át. Ez a form
action miatt történik.
A HTML listák és legördülők előállítási képességének tudománya itt meg is áll. Sajnos a ListItem sem
tud ennél többet. Pedig a <select> még számos további lehetőséget rejt. Ott van az <option> elemek
egyenkénti engedélyezése és tiltása a disabled attribútummal, az <optgroup> amivel az elemeket lehet
szépen csoportokba foglalni. Ezek hiányoznak a helper támogatásából.
A logikai értékek kezeléséhez nyújt segítséget a CheckBox és a CheckBoxFor, amelyek közül megint
csak az utóbbival foglalkozunk. A használata olyan egyszerű, hogy kár lenne szaporítani a szót. Egy
hátránya van, hogy nem működik bool? (nullázható) típussal.
Az eredménye már érdekesebb, mert az elvárt <input type=”checkbox …> alá kapunk egy hidden mezőt
is, ráadásul azonos name értékkel. Ennek az az oka, hogyha a checkbox nincs bejelölve, nem kerül bele
a POST adatokba. Ami számunkra amúgy is mindegy lenne, mert a CheckBoxFor nem háromállapotú,
a boolean típus alapértelmezett értéke pedig false.
RadioButton
A rádió gombok generálása foreach ciklusba van szervezve, hogy minden lehetséges érték
kiválasztható legyen. Szüksége van a modell propertyre, és egy értékre, ami a rádió gomb kiválasztása
esetén az előbbi propertybe kerül, mint műveleti eredmény. Harmadik paraméterként lehetőség van
egy boolean értékkel meghatározni, hogy az elem kiválasztott-e vagy sem.
[HttpGet]
public ActionResult Hcheck(int? id)
{
return View(ActionDemoModel.GetModell(id ?? 1));
}
[HttpPost]
public ActionResult Hcheck(ActionDemoModel inputmodel, FormCollection formcoll)
{
var model = ActionDemoModel.GetModell(inputmodel.Id);
model.VIP = inputmodel.VIP;
if (inputmodel.KeyPurchase.Id != model.KeyPurchase.Id)
{
model.KeyPurchase = model.PurchasesList
.FirstOrDefault(v => v.Id == inputmodel.KeyPurchase.Id);
}
return View(model);
}
Az eddigi Html helpereknek az volt az alapvető jellegzetessége, hogy a konkrét helpert nekünk kellett
egy propertyhez meghatározni. A string típushoz TextBox, a booleanhoz CheckBox illett a legjobban.
Ezek a helperek bizonyos értelemben még a kontrol alapú fejlesztési elvet követték, ami alatt azt
értem, hogy egy feladatra készítünk egy elég merev megjelenítőt. Ez biztos, hogy a legjobb
performanicát adja, de nem elég dinamikus és nem könnyen kiterjeszthető. Ha már egy template alapú
rendszerrel dolgozunk, jobb ha a megjelenítés inkább kontextus függő, felülbírálható, mintsem
bedrótozott. A Html.Editor, Html.Display, Html.EditorFor, Html.DisplayFor a mostani téma tárgya. Az
egyszerűség kedvéért fókuszáljunk a típusos (…For) változatukra. Ezek azt tudják biztosítani, hogy az
MVC a HTML elemek generálásához a property típusához definiált megjelenítőt használja. Ez a
definíció mindössze annyit tesz, hogy megvannak az alapértelmezett megjelenítők, de ezeket
minimális munkával lecserélhetjük. Ez első közelítésre azt jelenti, hogy egy string típusú propertyhez
egy textboxot, egy bool típusúhoz egy checkboxot fog generálni. A megjelenítés és a validációs célzatú
attribútumoknál láttuk a DataType attribútum hatását. Akkor azt írtam, hogy ez egy különleges
„metainformátor”. Annyira különleges, hogy szintén meghatározza a propertyhez generált HTML-t is
ezekkel a szóbanforgó helperekkel kapcsolatban. A DataTypeAttribute legfontosabb paramétere az
azonos nevű DataType enum. Ezen felül ezek a szóban forgó Html helperek még érzékenyek a
UIHintAttribute paraméterére is. Itt egy értelmetlen definíció, csak hogy egyben lássuk, hogy mik
szabályozzák HTML generáló módszer kiválasztását:
[DataType(DataType.MultilineText)]
[DataType("SpecialType")]
[UIHint("OwnEditorTemplate")]
public string Address { get; set; }
EditorFor és DisplayFor
Ahogy a nevük is sugallja, vagy szerkesztési, vagy megjelenítési HTML markup-ot generálnak az adott
propertyhez. A kipróbáláshoz hozzuk létre a View-kat. Misem egyszerűbb, ha a modell alapján VS
sablonokkal tesszük ezt. A kontroller létrehozásához használjuk is ki a segítséget:
A létrejött kontrollerből csak az Index, Details, és Edit metódusokra lesz szükség. Az Index metódusban
a modell listájából öt elemet adunk a View-nak. A többi action megvalósítása sem szorul különösebb
magyarázatra.
6.8 A View - Beépített Html helperek 1-166
[HttpPost]
public ActionResult Edit(int id, FormCollection coll)
{
var model = TemplateDemoModel.GetModell(id);
if (this.TryUpdateModel(model))
return RedirectToAction("Index");
return View(model);
}
}
<tr>
<td>@Html.DisplayFor(modelItem => item.FullName)</td>
<td>@Html.DisplayFor(modelItem => item.Address)</td>
<td>
@Html.DisplayFor(modelItem => item.Email)
</td>
<td>@Html.DisplayFor(modelItem => item.TotalSum)</td>
<td>@Html.DisplayFor(modelItem => item.LastPurchaseDate)</td>
<td>
@Html.DisplayFor(modelItem => item.VIP)
</td>
<td>
@Html.ActionLink("Edit", "Edit", new { id=item.Id }) |
@Html.ActionLink("Details", "Details", new { id=item.Id })
</td>
</tr>
6.8 A View - Beépített Html helperek 1-167
A generált HTML:
<tr>
<td>Tanuló 1</td>
<td>Budapest 2. kerület</td>
<td>
<a href="mailto:proba@proba.hu">proba@proba.hu</a>
</td>
<td>345,45</td>
<td>2013.03.30.</td>
<td>
<input class="check-box" disabled="disabled" type="checkbox" />
</td>
<td>
<a href="/TemplateDemo/Edit/1">Edit</a> |
<a href="/TemplateDemo/Details/1">Details</a>
</td>
</tr>
Ha most kipróbáljuk az Index, Details, Edit actionöket és View-kat, kiderül, hogy egy egész jó kis modult
csináltunk néhány kattintással. A modell listázható, elemei szerkeszthetőek.
Kezdjünk el faragni, hogy előjöjjenek az EditorFor igazi képességei. Az első próba legyen az, hogy a
modell VIP property típusát tegyük nullázhatóvá.
Kaptunk egy legördülő listát, mivel a bool?-nak három értéke is lehet. Kezd érdekes lenni, mert ilyet a
Html.CheckBox nem tudott. Egy (káros) mellékhatása, hogy az Index.cshtml által szolgáltatott lista is
megváltozik, ahol a DisplayFor készítette el a markupot.
A következő lépés legyen az, hogy a VIP tulajdonságot kidekoráljuk az UIHint attribútummal és „Text”
paraméterrel ellátva.
Egy normál textboxot kapunk. Most a boolean típus szöveges reprezentánsaként (angol true/false)
tudjuk megadni a VIP értékét szerkesztői módban.
Az EditorFor a decimal típusú property szerkesztéséhez egy formázott szövegű textboxot készít, de rá
tudjuk erőltetni, hogy készítsen olyat, amit egyébként az integer számokhoz csinálna, azzal, hogy
megadjuk a típus nevét szövegesen.
UIHint nélkül, egyedi esetben is megadhatom a megjelenítési formát, mert azt az EditorFor második
(string) templateName paramétere is ugyan ezt a célt szolgálja. Az eredmény megegyezik az előzővel.
Az előző két esetben, ha olyan nevet adunk meg, amihez nincs megjelenítési sablon, akkor egyszerűen
figyelmen kívül hagyja, nem dob exception-t és az alapértelmezett viselkedést követi:
Ezek szerint a háttérben tényleg van egy template rendszer, előkészített sablonokkal, ami több
szempont alapján is el tudja készíteni el a HTML darabkát.
A sablonok kiválasztása normál esetben az adat típusneve alapján történik meg, de másképpen is
meghatározhatjuk a template nevét. A template név meghatározásához a következő próbákat teszi,
sorrendben az MVC: (az első találat nyer)
Van-e megadva név a helper templateName paraméterben és létezik is hozzá template fájl.
Van-e megadva név az UIHint attribútummal és létezik is hozzá template fájl vagy beépített
sablon.
Van-e DataType attribútum és az meghatározza-e a nevet (az MVC keretrendszeren belül).
Mi a property/modell típusának a nullázható neve.
Mi a property/modell típusának a normál neve.
Ha nem összetett típus, akkor „string” lesz a név.
Ha IEnumearable képes, akkor „collection” lesz a név.
Ha más interface, akkor „object” lesz a név
Végül újra megpróbálja az előző lehetőségeket, a típus ősein végiglépkedve.
6.8 A View - Beépített Html helperek 1-169
Még nézzünk meg egy táblázatot, hogy a property típusa alapján mi az alapértelmezett működés.
Mit lehet csinálni az MVC (szinte)minden egyes képességével? Felülbírálni! Végül is csak template-eket
kell készíteni a megfelelő helyzetre.
A kód alapú sablonok részleteibe most nem megyünk bele, mert igen bonyolult dolog tud lenni a meta
információk értelmezésével együtt. Viszont van egy nagyon egyszerű módja az editor és display
template-ek készítésének: gondoljunk rájuk úgy, mint egy típusos Partial View-ra. Amit azokban meg
tudunk csinálni az használható template-ként is.
Első lépésként nem kell mást tenni, mint a View mappáján belül készíteni egy DisplayTemplates
mappát a Display, DisplayFor és a DisplayForModel helperek számára felkínálandó sablonok tárolására.
Ezen kívül egy EditorTemplates mappát az Editor, EditorFor és az EditorForModel helperek
sablonjainak. Persze, ha csak szerkesztő sablonokat csinálunk, akkor nem kell a DisplayTemplates és
viszont. A template fájlok DisplayTemplates és EditorTemplates mappáit, a Partial View-khoz
hasonlóan, két mappában is keresi az MVC: az aktuális View mappában és a Views/Shared mappában.
A globális template-eket érdemes tehát a Shared alól nyíló DisplayTemplates vagy EditorTemplates
mappába tenni. Egy triviális példával szemléltetve a „TotalSum” propertyre ráraktam a UIHint
attribútumot, amivel előírtam, hogy használja a KEuro nevű template-et. Ezzel előírtam, hogy
DisplayFor(m=>m.TotalSum) esetén a DisplayTemplates, EditorFor(m=>m.TotalSum) esetén az
EditorTemplates mappában keresse a KEuro.cshtml fájlt és használja azt, ha megtalálta.
6.8 A View - Beépített Html helperek 1-170
[UIHint("KEuro")]
public decimal TotalSum { get; set; }
A DisplayTemplates/KEuro.cshtml tartalma:
@model decimal
@string.Format("{0:,##0.0000} KEUR",Model /1000)
Ezzel elértem azt, hogy az „TotalSum” értéke ezer euróban lesz megjelenítve. Hogy a szerkesztő mezőn
is hatásos legyen az UIHint, létrehoztam Az EditorTemplates/KEuro.cshtml fájlt is ezzel a tartalommal:
@model decimal
@Html.TextBox("", Model /1000, "{0:,##0.0000}") KEUR
A fenti Html.TextBox-nak nem adtam nevet ("" – üres string), mert a létrejövő HTML input nevét majd
az EditorFor fogja biztosítani. A keletkezett HTML darabka így nézett ki (a validációs adatoktól
megfosztva):
A fenti példákban a UIHint-el határoztuk meg a template nevét, ami szerintem egy jó módszer erre.
Ahogy a template név meghatározási logikájának a listájánál már írtam, a template-ek nevei
származhatnak a property típusának nevéből is. Így használhatjuk a decimal.cshtml fájlnevet is, egy
decimal típusú property számára.
DisplayTemplates/decimal.cshtml, EditorTemplates/decimal.cshtml
Ennek az előnye, hogy akár az egész alkalmazásban meg tudjuk határozni a típushoz tartozó
megjelenítési formát, de lehet a kontrollerekhez tartozó View mappánként is. Hasonló módon egy
modellhez is rendelhetünk sablonokat, mivel a típus neve ismert:
DisplayTemplates/TemplateDemoModel.cshtml, EditorTemplates/TemplateDemoModel.cshtml
Ez eddig egy nagyon leegyszerűsített bemutató volt. Az EditorFor-ral igen különleges grafikus
editorokat is létre lehet hozni. Még a fejezet bevezetőjében írtam, hogy nem sok beépített HTML 5
támogatás van az MVC 4-ben. Ez azonban nem korlátozhat minket. Egy összetettebb vezérlő template
megvalósítása következik, amivel az „TotalSum” és más decimal típusú property értékét lehet 1 és 400
között egy csúszkával beállítani. A template @model típusát természetesen más numerikus típusra is
állíthatjuk.
6.8 A View - Beépített Html helperek 1-171
@model decimal
@{ var htmlid=Html.Id(""); }
<script type="text/javascript">
$(function () {
$('#@htmlid').bind('mouseup', function () {
$('#inner_@htmlid').val($(this).val());
});
});
</script>
Mentsük a fájlt RangeEuro.cshtml néven, emiatt a UIHint-et is át kell állítani, hogy ezt használja
szerkesztési sablonnak.
[UIHint("RangeEuro")]
public decimal TotalSum { get; set; }
A template példakód igényel némi magyarázatot az id és a name képzés miatt. Az első EditorFor
példában szintén nem adtam meg a Html.TextBox-nak nevet, csak egy üres stringet. Ennek oka, hogy
a név és id képzés úgy történik, hogy az EditorFor meghatározza a nevet a property alapján (TotalSum),
amihez az MVC a template-ben levő helperenként hozzáragaszt egy aláhúzást és a vezérlő saját nevét.
(pl.:„TotalSum_belsoeditor”). Kivéve, ha üres stringet adok meg (pl. a HTML.TextBox-nak). Ekkor a
template-en belüli input mezőnk a „TotalSum” nevet és id-t kapja meg, aláhúzás nélkül. Erre a model
bindernek van szüksége, hogy tudja követni a beágyazási hierarchia szerint a beviteli mezőket.
A jelen példában nem használunk belső Html helpert, hanem csak natív <input> elemeket. Emiatt ezek
nevét és id-jét nekünk kell meghatároznunk. A név és id képzése nem lehet önhatalmú, mert a
template elvileg más propertykhez is kapcsolható (nem csak a TotalSum-hoz), és ekkor viszont fel kell
deríteni a property nevét. Könnyítésként az MVC rendelkezésünkre bocsájt két különleges Html helpert
a Html.Id-t és a Html.Name-et. Ezek szolgáltatják a konvenciónak megfelelő nevet és azonosítót. A
példában ezek is "" – üres stringet kapnak valódi név és id helyett, ami azonos eredményt ad, mint amit
a TextBoxFor("", ) –esetében már átnéztünk. A javascript kód feladata mindössze annyi, hogy a
csúszkát húzogatva aktualizálja a másik, egyébként „disabled”, azaz nem szerkeszthető, jobb oldali
textbox tartalmát.
Felvetődhet a kérdés, hogy miért van szükség Partial View-ra és a DisplayFor + EditorFor párosra, ha
mindkettő közel azonos eredményt ad? Fontos, hogy megértsük, hogy a Partial View egy vagy több
View-hoz köthető View centrikus megoldás. Az azt injektáló Partial(), vagy Action() helperben kell
megadni szövegesen, hogy melyik partial View-ra gondolunk. A DisplayFor és EditorFor viszont a hozzá
kötött modell tulajdonságán keresi a meghatározást, hogy milyen template-el kell dolgoznia.
6.8 A View - Beépített Html helperek 1-172
DisplayForModel és EditorForModel
<hr />
<div class="editor-fileld">
@Html.EditorForModel()
</div>
Nézzünk meg egy összefoglalót az eddig látottak felhasználásával. A cél az lesz, hogy a normál editor
nézetben megjelenjen a vásárlások listája és ott helyben lehessen ezek sorait is szerkeszteni. Ráadásul
az egészet úgy, hogy a View kódjának begépelését minimalizáljuk. Ezért mindent View sablonokkal
fogunk elkészíteni, mindössze ezeket alakítjuk majd át. Ez lenne a végcél:
[Display(Name = "Azonosító")]
public int Id { get; set; }
[Display(Name = "Cikkszám")]
[DataType(DataType.Text)]
public string ItemNo { get; set; }
[Display(Name = "Mennyiség")]
public int Quantity { get; set; }
#region Listafeltöltés
private static int tid; //next id
public static IList<TemplateDemoProductModel> CreateProduct(int parentid)
{
int count = rand.Next(5, 10);
var result = new List<TemplateDemoProductModel>(count);
for (int i = 0; i < count; i++)
{
result.Add(new TemplateDemoProductModel
{
Id = ++tid,
ItemNo = string.Format("szam-{0}/{1}-k{2,-3}",parentid, i, DateTime.Today.Day),
Quantity = rand.Next(1, 1000),
ProductName = string.Format("{0}{1}",
ProductNames[rand.Next(ProductNames.Length)], tid * 1001)
});
}
return result;
}
private static readonly string[] ProductNames =
new[] { "Szék", "Ágy", "Asztal", "Párna", "Tükör", "Polc" };
#endregion
}
@model IEnumerable<MvcApplication1.Models.TemplateDemoProductModel>
<tr>
<th>@Html.DisplayNameFor(model => model.ItemNo)</th>
<th>@Html.DisplayNameFor(model => model.ProductName)</th>
<th>@Html.DisplayNameFor(model => model.Quantity)</th>
</tr>
A <table> elemre sincs szükség, mert az majd az Edit.cshtml-be fogjuk írni.
A létrejött fájlból ki kell törölni a feleslegeket és a <div> elemeket értelemszerűen <td> -vel kell
helyettesíteni.
@model MvcApplication1.Models.TemplateDemoProductModel
<tr>
<td>
@Html.HiddenFor(model => model.Id)
@Html.EditorFor(model => model.ItemNo)
</td>
Az Edit.cshtml vége felé illesszük be a vastagon szedett sorokat, hogy használja az előbb elkészített két
sablont:
<div class="editor-field">
@Html.EditorFor(model => model.VIP)
@Html.ValidationMessageFor(model => model.VIP)
</div>
<hr />
<div class="editor-fileld">
<table>
@Html.DisplayFor(m=>m.PurchasesList,"TemplateDemoProductModelHeader")
@Html.EditorFor(m=>m.PurchasesList)
</table>
</div>
Eddig volt a varázslat, most következik a tudomány. A fenti kódban létrehoztam egy táblázatot. Ennek
a fejlécét a TemplateDemoProductModelHeader-ben található trükk fogja létrehozni. A DisplayFor
számára megadtam a használandó template nevét, mert ha később egy nem szerkeszthető oldalt is
szeretnénk, annak jobb lesz a TemplateDemoProductModel template nevet fenntartani. Az EditorFor
sorában nem adtam meg nevet, mert az előbb létrehozott TemplateDemoProductModel.cshtml
6.8 A View - Beépített Html helperek 1-175
sablont fogja használni mivel a PurchasesList lista elemeinek típusa is ugyan ilyen nevű. Nem tudom,
hogy mennyire észrevehető, de a TemplateDemoProductModel template modell típusa egy osztály és
nem egy felsorolás, lista vagy tömb, mégis az EditorFor számára a PurchasesList kollekciót tartalmazó
propertyre hivatkoztam. Az EditorFor belső kódja ezt észreveszi és ilyen esetben, a lista elemein
végigiterál és az elemeit a template alapján fogja feldolgozni. Emiatt jönnek létre a táblázat sorai. Az
egészben a nagyszerű mégsem ez a megoldás önmagában, mert ezt a táblázatos szerkesztőt sok más
módon is meg lehetett volna oldani. A nagyszerűség abban rejlik, hogy a táblázat sorait szerkesztve,
működik a mentés is.
<table>
@Html.DisplayFor(m=>m.PurchasesList,"TemplateDemoProductModelHeader")
@for(int i = 0; i < Model.PurchasesList.Count;i++ )
{
@Html.EditorFor(m => m.PurchasesList[i])
}
</table>
Amit még érdemes megnézni az a generált HTML részletben levő <input> elemek id-jei és nevei. Az
alábbi kódból kitöröltem néhány mezőt és a validációs és class attribútumokat, hogy a lényeg jobban
látható legyen. Sokat nem is magyaráznám, mert gondolom egyértelmű az elnevezési konvenció egy
kollekció renderelése esetén.
<tr>
<td>
<input id="PurchasesList_0__Id" name="PurchasesList[0].Id" type="hidden" value="1" />
<input id="PurchasesList_0__ItemNo" name="PurchasesList[0].ItemNo" type="text" value="szam0-k6-
1"/>
</td>
...
</tr>
<tr>
<td>
<input id="PurchasesList_1__Id" name="PurchasesList[1].Id" type="hidden" value="2" />
<input id="PurchasesList_1__ItemNo" name="PurchasesList[1].ItemNo" type="text" value="szam1-k6"
/>
</td>
...
</tr>
6.8 A View - Beépített Html helperek 1-176
Az indexelőnek nem muszáj számnak lennie, lehet például Guid is. Mivel csak a fenti elnevezési
konvenció a fontos más módszerrel is elő lehet állítani a neveket és az id-ket. Erre következzen megint
egy verzió.
Ennél a formánál megadtam az editor template nevét is az idéző jelek között, de emiatt a név és id
generátornak az elnevezést is át kell adni ("PurchasesList[" + i + "]"). Mindenesetre ezzel a lehetőséggel
az editorok és esetleg partial View-k hierarchiájában bárhol biztosítani lehet az <input> mező nevek és
id-k pontos definícióját.
A „variációk egy témára” tartogat még megoldási lehetőségeket. Azonban ez már csak egy pici változás
az előzőhöz képest. Ez abban az esetben hasznos, ha nem áll rendelkezésre egy indexelhető lista, csak
egy IEnumerable képességű osztály. Egy „i” segédváltozót beiktattam, de ez lehetne akár a
modellosztályban is.
@{ Html.RenderPartial("DetailList", Model.PurchasesList); }
@{Html.RenderAction("ChildAction"); }
Normál verzió: @Html.Action("ChildAction")
Ha a kedves olvasó még nem unta meg az EditorFor felhasználási variációit, álljon itt egy további
összetett példa, ami a RenderPartial és az EditorFor együttműködését szemlélteti. Úgy érzem
szükséges ezt a témát a lehetőségekhez képes több formában körüljárni, mert a tapasztalatom az, hogy
a megvalósult MVC alkalmazásokban igen gyakran vannak felhasználva.
6.8 A View - Beépített Html helperek 1-177
Továbbra is a TemplateDemo View mappájában levő Edit.cshtml-t kéne bővíteni most mindössze
ezzel az egy sorral:
@{ Html.RenderPartial("TemplateGrid", Model.PurchasesList); }
Természetesen az esetleg ott levő EditorFor felhasználását érdemes kikommentezni.
Ebből következőleg szükségünk lesz egy TemplateGrid típusos partial View-ra. Ezt megint csak a View-
t generáló dialógusablakban érdemes előállítani. A View fájlnak, most nem a template könyvtárban
van a helye, hanem az Edit.cshtml mellett. Mivel a létrejött View tartalma megint nem pont olyan, mint
amire szükség van, ezért kicsit át kell alakítani. Ez nagyrészt ismét csak törlést jelent.
@model IEnumerable<MvcApplication1.Models.TemplateDemoProductModel>
@{ int i = 0; }
<table>
<tr>
<th>
@Html.DisplayNameFor(model => model.ItemNo)
</th>
<th>
@Html.DisplayNameFor(model => model.ProductName)
</th>
<th>
@Html.DisplayNameFor(model => model.Quantity)
</th>
</tr>
A működése az előző fejezet komplex példáját követi. A lényeges különbség, hogy most nincs szükség
a @Html.DisplayFor(m => m.PurchasesList, "TemplateDemoProductModelHeader") sorra, mivel ennek
a funkcionalitását, most a TemplateGrid nevű View látja el az első néhány sorában. Továbbá a teljes
tábla definíció is itt van. Ezzel a példával szerettem volna érzékeltetni a partial View és a modell alapú
típusos editor és display template-ek együttműködésének hasznosságát. Mind a template alapú
EditorFor (+DisplayFor), mind a partial View normál View-ba ágyazása jó tervezéssel párhuzamosságot
tart fenn a modellosztály és az abba beágyazott további osztályok és kollekciók hierarchiájával. Ezt
azért emelem ki, mert ez a megközelítés könnyebb megérthetőséget és karbantarthatóságot biztosít
összetett, egymásba ágyazott modell és View struktúráknál.
6.8 A View - Beépített Html helperek 1-178
Láttunk példát az adat érvényesség vizsgálatára a 4.3.2 Validáció fejezetben, amikor a modell
propertyjeinek a validációs attribútumait próbálgattuk. Akkor az „csak úgy magától működött”, de nem
sokat foglalkoztunk azzal, hogyan jeleníthető meg a hibaüzenet a weblapon.
ValidationMessageFor és ValidationSummary
Amíg nincs validációs hiba, ezek a helperek egy helyőrzőként funkcionáló HTML szakaszt illesztenek be.
Hiba esetén látható az eredményük. Nézzünk egy View-t a felhasználáshoz. A vastagon szedett részek
a fontosak. A használata megegyezik az eddig látott, típusos („For” névutótagú) Html helperek
használatával. Ahova tesszük, ott fog megjelenni a validációs hibaüzenet. (A kikommentezett
ValidationSummary-val később foglalkozunk)
@model MvcApplication1.Models.ValidationDemoModel
<h2>Validációs helperek</h2>
@*@Html.ValidationSummary()*@
<br />
@using (Html.BeginForm(null, "Helper", new { id = Model.Id }, FormMethod.Post))
{
@:Szöveg: @Html.TextBoxFor(m => m.FullName) <br />
@Html.ValidationMessageFor(m=>m.FullName)
<br />
@:Multiline<br />@Html.TextAreaFor(m => m.Address, 4, 20, null)
@Html.ValidationMessageFor(m=>m.Address)
<br />
<input type="submit" />
}
A modell azonos propertyket és szabályokat tartalmaz, mint amit a 4.3.2 fejezetben a validációs
attribútumoknál kipróbáltunk, de azért kiemeltem a fontos propertyk listáját. A tömörség kedvéért
csak a validációs attribútumok maradtak.
[Required(ErrorMessageResourceName = "AddressRule",
ErrorMessageResourceType = typeof(Resources.Validations))]
public string Address { get; set; }
[Range(100.1, 200.1)]
public decimal TotalSum { get; set; }
[Range(typeof(DateTime),"2010.01.01","9999.12.31")]
public DateTime LastPurchaseDate { get; set; }
[HttpGet]
public ActionResult Hvalid2(int? id)
{
return View("Hvalid", ValidationDemoModel.GetModell(id ?? 1));
}
[HttpPost]
public ActionResult Hvalid2(ValidationDemoModel inputmodel)
{
if (ModelState.IsValid && inputmodel.Id > 0)
{
var model = ValidationDemoModel.GetModell(inputmodel.Id);
model.Address = inputmodel.Address;
model.FullName = inputmodel.FullName;
return View("Hvalid", model);
6.9 A View - UrlHelper 1-179
Mire a Hvalid2 post actionhöz megérkezik a vezérlés a ModelState fel lett töltve a validációs hibákkal,
az IsValid értéke ezért false. A ModelState egyben egy dictionary is, ami a fenti esetben - mivel
következetesen működött a model binder - három(!) hibát tartalmazott. Hibás a FullName mező,
aminek az eredménye látható a fenti képen. És ott van még a TotalSum és az LastPurchaseDate is,
amik szintén hibásak. A formon nincsenek rajta input mezőként. Így esélyem sem volt, hogy valid
adatot adjak meg. Ezeknek a validációs hibaüzenetét nem látjuk, mert a View-ba nem tettünk hozzájuk
property szintű ValidationMessageFor helpert. Szedjük ki a kommentet a
@Html.ValidationSummary()
sorból. Innentől meg fog jelenni ennek a helyén az összesített hibaüzenet, ami tartalmazza az összes
validációs hibát.
Ez a példa egy további részletre is rámutatott. Amiatt, hogy az action metódus paramétereként a
típusos modellt várjuk, a model binder a modell példány feltöltése során a validációt érvényesítette a
teljes modellre. A „Név nem tartalmazhat számot” egy metódus alapú validáció üzenete volt. Így
kibukott a további két, a View-n nem használt property validátor is.
Ez most megint csak egy snitt volt a validáció egész estés mozijából. A validáció problémaköre még
mindig nincs megfelelő mélységben megvilágítva, ezért ennek egy külön fejezet készült (9.5).
6.9. UrlHelper
Ahogy az ActionLink a BeginForm esetében láttuk azt, hogy a kontroller és action nevéből nem érdemes
ad-hoc módon URL-t összekolbászolni, így használhatjuk az UrlHelper metódusait arra, hogy
alkalmazáson belüli URL-eket hozunk létre. Mellesleg az ActionLink és a BeginForm is az UrlHelper
belső metódusait használja. Az UrlHelpert az Url propertyn keresztül lehet elérni a View kódjában és
ugyan ilyen néven lehet elérni a kontroller kódján belül is. A benne található metódusok tudnak az
aktuális request paramétereiről ezért képesek alkotni relatív és abszolút URL-eket is. Nézzük a
metódusait röviden.
Action
<p>
A link: @Url.Action("AnotherAction")
</p>
<p>
Manuális link: <a href="@Url.Action("AnotherAction")">Ez a belső action linkje
<img src="~/Images/orderedList0.png">
</a>
</p>
<p>
Javascript eseménykezelő: <img src="~/Images/orderedList1.png" id="id1" style="cursor: pointer">
<script type="text/javascript">
$(function () {
$('#id1').on('click', function () {
window.location = '@Url.Action("AnotherAction")';
});
});
</script>
</p>
/urlhelper/AnotherAction
Lehetőség van Url paraméterek megadására is. Az alábbi kód eredménye az alatt látható.
RouteUrl
A 2. sor számára a bejövő request protokoll nevét adtam át. Ez most http volt. Viszont ezt csak teljes
URL-el lehet megjeleníteni, ezért hozzáadta a domainnév:portszám URL részletet is.
Encode
Előfordul, hogy az URL-be olyan paramétert szeretnénk tenni, ami nem felel meg az URL-re vonatkozó
szabályoknak. A nem megfelelő karaktereket HTML entitásokkal kell helyettesíteni. Az alábbi két érték
elkódolt változata és alatta az eredménye látható, amit fel lehet használni URL paraméterként is.
category=@Url.Encode("arm chair")&codes=@Url.Encode("<9999>")
category=arm+chair&codes=%3c9999%3e
6.9 A View - UrlHelper 1-181
Content és a tilde ~
Ez a Content helper igen hasznos számunkra, mikor egy fizikailag létező fájlra hivatkozó URL-t
szeretnénk készíteni. Az MVC 4-ben bevezetett újdonságok egyike, hogy bárhol, ahol URL-t akarunk
megadni, kezdhetjük azt a tilde ~ karakterrel, ami a Content szükségességét leállósávra tette. Hogy
részletesen be tudjam mutatni a hatását, el kell térni a „miénk a komplett site, tiéd a lekvár” IIS
konfigurációtól. A fejlesztési gépen, mivel a webszerver teljesen a fennhatóságunk alatt van, nem kell
foglalkozni az IIS konfigurációjával. Aztán – mivel tegyük fel nem közölték velünk, hogy az éles
szerveren virtuális mappába fogják telepíteni – az alkalmazásunk futásának eredményeként egy
összetört, ikonmentes, zavaros oldalt kapunk, jó esetben. Az alábbi, képet megjelenítő markup
működni fog a mi fejlesztői környezetünkben, de ha egy virtuális mappába telepítjük a webalkalmazást,
akkor nem biztos.
Ez a változat viszont működni fog, függetlenül attól, hogy virtuális mappában van az alkalmazásunk
vagy nem.
A virtuális path jelentősége abban rejlik, hogy egy domain név alá több alkalmazást is tudunk telepíteni,
amelyek teljesen más fizikai mappában vannak.
Az utolsó példa szerint olyan konfiguráció is lehetséges, hogy a domain név alá tartozó virtuális
útvonalak kiszolgálását teljesen más fizikai képen futó webszerverek végzik. Ehhez proxy szerver vagy
URL rewite konfigurálás szükséges. A virtuális szervezésnek számos előnye van pl.
Azonban mint már említettem az MVC 4 óta nincs szükség az Url.Content-re, a következő sor is jól fog
működni ettől a verziótól kezdve:
Összefoglalásképpen nézzük meg együtt az egészet még mindig egy virtuális mappában elhelyezve az
alkalmazásunkat, azaz a View sorok egy virtuális web alkalmazás konfigurációban vannak értelmezve.
A jobb oldali képen, az első sorban a „Link direkt” után is egy kettes számnak kellene állnia, de mivel
abszolút útvonalon határoztam meg a kép útvonalát, így nem érhető el a böngésző számára.
A próbák után ne felejtsük el visszaállítani az MVC alkalmazásunk projekt konfigurációját „Local IIS
web” szerverre.
IsLocalUrl
Ez egy problémás helper. Nem javasolt a használata és az alábbi kódból kiderül, hogy miért.
A 3. esetében, érdekes módon True az eredmény, pedig az nyilvánvalóan nem is egy normális URL,
csak / jellel kezdődik. Ráadásul az értelmezhető része sehogyan sem „local”.
Annyit ér a használata, hogy egy URL-ről megmondja, hogy '/' vagy '~/' jellel kezdődik-e.
7.1 Aszinkron üzem, AJAX - Keretrendszerek tárháza 1-183
Az eddigiek során a HTML előállítását egy komplex lépésben tettük meg. Használtunk ugyan partial
View-kat és child actionöket, de ezek eredménye egy darab HTTP response lett, ami mindent
tartalmazott, amit a böngészőnek meg kellett jelenítenie, mint kiinduló HTML markup. Ez az
alapmódszer egy szintig megfelelő, de elérkezik az a pont, amikor a mai trendeknek megfelelően
nagyon interaktív és gyors weblapokat szeretnénk készíteni. Olyat, amikor az oldal részletei külön
életet élhetnek.
És néhány hátránnyal:
Jóval több JS kódra és több odafigyelésre lesz szükség. Legalábbis az elején nehézkesen szokott
menni, aztán ráérzünk az ízére és nem is akarunk front-end-et megvalósítani más módon csak
AJAX-al.
Nagyon képben kell lenni a trendi technikákkal és a jó megvalósításokkal, best-practice-ekkel.
A HTML és CSS trükkökkel. Az utóbbi időkben a böngészőkben történt fejlesztések fő
csapásiránya a javascript feldolgozó motor gyorsítása volt és maradt. Ugyanis a jó web oldalak
minőségét a JS kód futási sebessége nagyban befolyásolja. Mivel a feldolgozó motor képessége
és a futtató hardver sebessége is véges, ezért az általunk írt JS kódot optimalizáltan kell
megírni, takarékoskodva az erőforrásokkal.
Az AJAX egyáltalán nem új keletű dolog. Ráadásul külön életet él az .NET és MVC világától, bármely
webes technológiában alkalmazhatjuk. Ennek a kettő tények van néhány nagyszerű hozománya: mára
kiforrott a használatának módja, tömegével állnak rendelkezésre referenciák, ötletek, best practice-ek
bármilyen helyzetben is szeretnénk használni. Mivel a működés alappillére, hogy a kliens oldalon egy
javascript kód gondoskodik az oldal részlet feldolgozásáról, betöltéséről, szükséges, hogy valami
egységes, generikus megoldásunk legyen, és ne kelljen oldalról-oldalra újra, egyedileg kódokat írnunk
a HTML oldalrészletek kezelésére. Emiatt és a HTML elemek kezelésének ismétlődő feladatainak
7.2 Aszinkron üzem, AJAX - A JSON 1-184
lefedésére (DOM 30manipulálásra, bejárására) számos javascript keretrendszert 31 hoztak létre. Olyan
sokat, hogy nem is tudok olyanról, aki ismerné mindet, de legtöbbünk vélhetően nem tudná felsorolni,
csak néhánynak a nevét. A kínálat óriási, csak el kell döntenünk, hogy melyik legyen a kedvenc és utána
azt használhatjuk szinte mindenre. Biztos vagyok benne, hogy a lényeges és gyakori feladatokat a
legtöbb jól el tudja látni. Mivel ezt a túlkínálatot senki sem bírja átlátni, az évek során kialakultak a
szerver oldali és a kliens oldali framework párok. Az ASP.NET MVC-nek de facto párja a jQuery. És ez jó
párosítás, figyelembe véve azt, hogy a közelmúltban a jQuery által használt kiválasztási formát
(selector) szabványosították. Ezt a legtöbb böngésző natívan támogatja, emiatt a sebesség
szempontjából jó előnye van annak, aki ezt a framework-öt választotta. Ha már megnéztük az MVC
projekt mappáiban a Scripts mappa tartalmát, akkor ismerős lehet, mert bizony ott van a jQuery is. A
_Layout.cshtml végén található az a sor, ami hozzákapcsolja az összes oldalunkhoz ezt a
keretrendszert.
7.2. A JSON
Ha van keretrendszer, ami egy alkalmazás réteg, akkor kell lennie szabványos adatformátumnak is, ami
a többi réteg közti adatcserét biztosítja. Nem olyan régen még úgy tűnt, hogy az univerzális gép-gép
kommunikációs nyelv az XML lesz. Többek között a böngésző és a szerver közti aszinkron adatcserét
lebonyolító technológia is. Az AJAX elnevezése is erre utal (Asynchronous JavaScript and XML). Ehelyett
azonban a JS adatábrázolásához jobban illeszkedő JavaScript Object Notation szöveges megjelenítése
terjedt el. Az AJAX-ot ennek ellenére nem nevezték át AJAJ-ra, hiszen az AJAX már bejáratott
technológiai kifejezéssé vált, annak megváltoztatása csak összezavarta volna az embereket. Az AJAX
ezért nem szó szerint értendő terminológia.
Nincsenek záró tagek, attribútumok, séma definíciók. Nincs is rá szükség, hiszen az adatot mind a
szerver, mind a kliens oldalon mi kezeljük a saját magunk által definiált séma alapján. A további
részletek mellőzésével ajánlom tanulmányozásra a http://json.org/example.html oldalt. Érdemes
megnézni az ott található példák alapján, hogy miért előnyösebb a JSON az XML-nél ebben a böngésző-
webszerver relációban. Részletesen bemutatja a különböző adattípusok és adatábrázolások
megvalósítási példáit.
A DOM/HTML kezeléséhez, kódból való alakításához annak elemeit el kell érni. Ez sokszor nem is olyan
egyszerű javascriptből, tetszőleges böngészőn futtatva. A getElementById és a getElementsBy…
30
DOM: Html dokumentum objektum modell. http://hu.wikipedia.org/wiki/Document_Object_Model
31
http://en.wikipedia.org/wiki/Comparison_of_JavaScript_frameworks
32
http://www.json.org/
7.3 Aszinkron üzem, AJAX - jQuery dióhéjban 1-185
metódusok használata nagy tételben megnehezítik a munkát. Viszont létezik a CSS szabvány, ami a
HTML elemeihez ad stílusinformációt. A CSS-ben egyáltalán nem bonyolult összekapcsolni a stílust a
HTML elemekkel. Lehet hivatkozni a HTML elemek osztályára (.) id-jére (#) relatív helyzetére, stb. Ehhez
nagyon hasonlót fogad a jQuery szelektora is. Ahhoz, hogy el tudjuk rejteni a
További példaként: az oldalunkon levő összes ’szovegesmezo’ osztállyal ellátott elemet egyszerre el
akarjuk rejteni a következő sor megteszi ezt:
$(’.szovegesmezo’).hide();
$(’.szovegesmezo’).show().val(’’);
A $(’.szovegesmezo’) függvény visszatérési értéke jQuery objektum, amin meghívjuk a show()
metódust, aminek a visszatérési értéke jQuery objektum, amin meghívjuk a val(’’) metódust.
Az HTML elemek megtalálása és manipulálása ennyire egyszerű, és szinte az összes helyzetre van kész
megoldás. Legyen szó attribútumok, osztályok, HTML tartalmak hozzáadásáról, eltávolításáról.
Létezik még a „hogyan kezeljük az eseményeket” problémakör. A régi megoldás, amit a HTML szabvány
támogat, hogy az eseménykezelő kódot valamelyik onEseménynév attribútumban adjuk meg. Ennek
egy kulturáltabb megjelenése, amikor csak egy függvényhívás szerepel benne:
Így csak egy függvényben kell megírni a JS kódot. Azért látható, hogy ez eléggé csúnya még így is, mert
még mindig függvénynév van a html markupban. Ebben is tud segíteni a jQuery, mert a szelektorral
kiválasztott HTML elem(ek) valamelyik eseményére fel tudunk iratkozni. Például, ha szeretnénk
feliratkozni, az összes ’szovegesmezo’ osztályú elem egérkattintás esetén bekövetkező OnClick
eseményére, akkor elég a következő sor:
$(’.szovegesmezo’).click(function(){
$(this).css(’background-color’,’red’);
}
A kattintott elem háttérszínét pirosra változtatja. A click() metódus paraméterében egy funkciót vár,
amit ilyen tömör formában, anonymous funkcióban is lehet deklarálni. A fenti példa helyett írhattam
volna ezt is, az eredmény ugyan az:
A problémánk már csak az lehet, hogy mikor iratkozzunk fel az onclick eseményre? Akkor érdemes,
amikor a teljes oldalt a böngésző véglegesre összeállította, a DOM elkészült (ready), de még nem jelent
meg a felhasználó számára. A jQuery erre is ad megoldást. Az előbbi feliratkozást beágyazhatjuk abba
az eseménykezelőbe, ami akkor fut le, mikor a HTML dokumentum elkészült.
$(document).ready(function(){
$(’.szovegesmezo’).on( ’click’,function(event){
$(this).css(’background-color’,’red’);
}
});
Ránézésre biztosan meg tudná mondani egy HTML-hez is értő dizájner, hogy a „halvanyulo” osztály
talán azért van ott, mert egy eseménykezelő feliratkozott rá? Példaként a következő igény az lenne,
hogy a halványulás sebességét egyedileg szeretnénk meghatározni, némely <a> linknél (a fenti példa
esetén talán egy gomb). Ehhez kell egy egyedi paraméter. Ezt hol definiáljuk? Nos, ilyen és hasonló
okokból kezdték alkalmazni a deklaratív megközelítést, amikor a HTML elem kódhoz kapcsolását és a
kód számára fontos paramétereket a data-* kezdőnevű attribútumokkal jelölik. Ez nagyon hasonló a
.Net attribútumaihoz, amivel az osztályhoz vagy a propertyhez metaadatokat rendelhetünk és később
a kódban ezeket lekérdezhetjük.
Erről a HTML linkről a színezés nélkül is könnyű megmondani, hogy mi tartozik a deklaratív kódoláshoz
és mi a dizájnhoz:
33
http://en.wikipedia.org/wiki/Unobtrusive_JavaScript
7.4 Aszinkron üzem, AJAX - Ajax helperek 1-187
Az MVC nagyon épít erre a megközelítésre. Viszont, hogy ez működjön, két dologra van szükségünk.
Az egyik, hogy a gyökér web.config-ban engedélyezve legyen:
<appSettings>
<add key="ClientValidationEnabled" value="true"/>
<add key="UnobtrusiveJavaScriptEnabled" value="true"/>
</appSettings>
A másik, hogy a jQuery unobtrusive kiegészítést mellékeljük az oldalunkhoz. Ezt vagy a Layout.cshtml-
ben vagy akár a View-ban is megtehetjük:
@section Scripts {
@Scripts.Render("~/bundles/jqueryval")
}
Az ajaxos oldalak felépítésekor néhány tipikus, ismétlődő helyzettel találkozhatunk, amire jó ha vannak
generikus megoldásaink:
Egy felhasználói interakcióra válaszként szeretnénk az oldal egy darabját újratölteni. Az ilyen
interakciók lehetnek például a táblázatlapozások és sorrendezések, egy gombra vagy a
táblafejlécre kattintva.
Nem újratölteni akarunk, hanem a tartalmat bővíteni, kiegészíteni. Ilyennel találkozhatunk,
amikor egy okos képgaléria mini képeit „csúsztatjuk”, amivel újabb képek betöltését indikáljuk.
Vagy az oldal alját elérve további x darab hozzászólást tudunk lekérni.
Egy kitöltött formot, ami csak az oldal egy részlete, szeretnénk beküldeni. Például, egy login
ablakocska.
Egy előugró modális „ablakban” szeretnénk tartalmat megjeleníteni. Az ilyen tartalom lehet
egy form is.
Mivel az ajaxos művelet egy külön (XHR34) világban zajlik, egy oldalrészlet letöltését felügyelni
szükséges. Emiatt egy ajax hívásra érkező választ értelmezni kell több szempont alapján:
o Sikeres vagy hibás a kérés. Ilyenkor nem a felhasználót kell tájékoztatni egy
hibaoldallal, hanem a kódnak kell értelmeznie a helyzetet.
o A megérkezett válasz teljes vagy még további részletek érkeznek? Esetleg újabb ajax
kérést kell foganatosítani?
o A sikeresen érkezett JSON adatokkal utómunkát kell végezni. Átalakítani, formázni,
vagy feltölteni a HTML elemeket.
o Lehetséges, hogy a sikeresen érkezett HTML tartalom elemeinek eseményeire fel kell
iratkozni.
34
XmlHttpRequest
7.4 Aszinkron üzem, AJAX - Ajax helperek 1-188
Amint érzékelhető, nem is olyan triviális a helyzet, amikor belépünk ebbe a dinamikus világba. Érdemes
úgy gondolni az ajax működésű oldalakra, mint egy valódi kliens-szerver architektúrában felépített
rendszerre. A böngészőben, mint kliensben, összetett javascript kódok felelnek azért, hogy a felsorolt
helyzeteket kezelni tudjuk.
A View-kat tárgyaló fejezetben látott hagyományos Html helperek mellett léteznek beépített Ajax
helper metódusok, amik támogatják az ajax speciális eseteinek használatát. Ebből a két legfontosabb
az AjaxHelper.ActionLink és az AjaxHelper.BeginForm, amik a View Ajax tulajdonságán keresztül
érhetők el. Mindkettőre jellemző, hogy a használatuk alig tér el a hagyományos Html helper
névrokonaitól. A beépített helperek a jQuery framework-re támaszkodnak és annak is az „unobtrusive”
megközelítését szeretik használni.
ActionLink
A feladata, hogy az általa generált linkre kattintva ne a böngésző navigáljon a megadott action által
képviselt URL-re, hanem az action/View által generált tartalmat az oldalunkba ágyazza. Ez lefedi a HTTP
get metódussal lekérhető tartalmak ajaxos feldolgozását. Egy példa alapján nézzük mire képes:
Látható, hogy rendelkezik a szokásos paraméterekkel. Az action név (Details) és a route adatok (id=..)
ismerősek már. A szerepük teljesen egyezik a Html helperes társánál megszokottakkal. A lényeg az
AjaxOptions csomagban rejlik. Ezzel határozhatók meg az ajax-specifikus helyzetek és adatok. A
következő tulajdonságokkal rendelkezik:
Confirm – Az oldal letöltés előtt egy Yes-No dialógusablakban a benne tárolt szöveget megjeleníti.
Természetesen a No-ra kattintva nem hajtja végre az ajax letöltést. Mivel a normál
window.confirm(szöveg) javascript dialógust használja, ezért ennek megfelelő stílusú ablakot várjunk.
HttpMethod – Post vagy Get mód. Ha nem adjuk meg a default a Get mód lesz az alapértelmezett.
UpdateTargetId – Annak a HTML elemnek az Id-je, ahova a letöltött tartalmat szeretnénk tenni.
LoadingElementId – Egy HTML elemet, jellemzően egy rejtett div-et határozhatunk meg, ami arra az
időre fog megjelenni, amíg a válasz meg nem érkezik a szerverről. Ez általában egy „kérem várjon” vagy
egy forgó animált gif (ajax-loader.gif)35 szokott lenni.
35
A több online ajax loader gif generátorok egyike: http://www.ajaxload.info/.
7.4 Aszinkron üzem, AJAX - Ajax helperek 1-189
Url – Bár az ActionLink action és kontroller paramétere meghatározza a célt, ezzel a paraméterrel ezt
felül tudjuk bírálni.
OnBegin – Meghatározhatunk egy JS callback funkciót, ami meghívásra kerül az ajax request előtt. Az
összes On prefixszel kezdődő további paraméterek is JS eseménykezelőket határoznak meg.
OnComplete – A válasz (response) megérkezése után, de még az UpdateTargetId által jelölt HTML elem
feltöltése előtt futtatandó funkció nevét lehet megadni.
OnFailure – HTTP hiba esetén kerül meghívásra az OnComplete és az OnSuccess helyett. Ez egy
lehetőség a hibakezelésre, amit nekünk kell menedzselnünk. A böngésző még az 5xx-as HTTP hibákat
sem fogja lekezelni.
Ezeknek megfelelően az előbbi példa AjaxOptions paraméterei szerint egy get metódussal kérjük el a
tartalmat, amit a „popupdiv” id-jű HTML elembe helyeztetünk felülírva annak tartalmát. A feltöltés
előtt le fog futni az openPopupDialog és utána az closePopup funkció:
new AjaxOptions()
{
HttpMethod = "get",
InsertionMode = InsertionMode.Replace,
OnBegin = "openPopupDialog",
OnComplete = "closePopup",
UpdateTargetId = "popupdiv"
})
Jó lenne, de nem képes az Ajax.ActionLink a letöltött tartalmat közvetlenül egy modális ablakban
megjeleníteni. A példa kedvéért mégis csináljuk ezt meg. A tartalom placeholder-e a „popupdiv” ide
várjuk a szervertől jövő tartalmat, ami egy Detail View tartalom lesz a már használt
TemplateDemoModel példányból.
A display:none azért szükséges, hogy a div-hez rendelt esetleges stílusok ne legyenek láthatóak, amíg
a dialógus ablakot meg nem nyitjuk.
Amíg a Details tartalma letöltődik, a felhasználót szórakoztassuk egy „Betöltés…” szöveggel, ami
szintén egy modális dialógusban jelenik meg:
@section Scripts {
@Scripts.Render("~/bundles/jqueryval")
@Scripts.Render("~/bundles/jqueryui")
}
Az unobtrusive is kell természetesen.
function openPopupDialog() {
$('#betoltespopup').dialog({
autoOpen: true,
width: 600,
height: 100,
modal: true,
resizable: false,
hide: {
effect: "blind",
duration: 300
},
close: function() {
$('#popupdiv').dialog({
autoOpen: true,
width: 600,
height: 'auto',
modal: true,
show: {
effect: "blind",
duration: 300
}
});
}
});
}
A kód a következőket teszi: A betoltespopup id-jű div-ből dialógusablakot csinál a paraméter csomagja
által meghatározva. A csomag jellemzői: Azonnal megnyílik (autoOpen), szélessége 600px, magassága
100px, modális, nem méretezhető. A bezáródás egy 300ms alatt lejátszódó „roló felhúzás” effektussal
történik (hide:blind). Na, ez lesz a zöld feliratos Betöltés ablak, ami megnyílik még az ajax kérés előtt.
A bezárását majd az OnComplete esemény closePopup funkciója fogja elvégezni (trükkösen).
function closePopup() {
$('#betoltespopup').dialog("close");
}
Tehát miután megérkezik az ajax válasz, feltöltésre kerül a popupdiv és utána meghívásra kerül ez a
funkció, ami bezárja a betöltés dialógusablakot. A trükk, hogy a '#betoltespopup' dialógus ablaknak is
van egy close eseménye, ld. fent kiemelve. Ez viszont megnyitja a hasznos, és időközben feltöltött
popupdiv dialógusablakot benne a táblázattal. A dialógus ablakban megjelenhet egy szerkesztő form
is.
7.4 Aszinkron üzem, AJAX - Ajax helperek 1-191
Azonban ez már továbbvisz minket a következő témára az ajax alapú űrlapkezelésre és a helper
metódusaira.
BeginForm, EndForm
Az ActionLink után ez már nem okozhat meglepetést. A paraméterei szintén ismerősek az azonos nevű,
normál Html extension metódusokból. Szintén igényel egy AjaxOptions csomagot. A különbség
mindössze annyi, hogy ez a post metódussal elküldi a felépített formban levő input mezőket.
Egyszerűsített példa:
<div id="updatablelist">
@Html.Partial("IndexListPartial", Model)
</div>
A form működése egyszerű, a „Keress” feliratú gomb megnyomásával a formot elküldi a szervernek a
'findName' és 'findAddress' nevű textbox tartalmával. A visszajövő választ a <div id=”updatablelist”> -
be teszi, mert ez van az UpdateTargetId-ben meghatározva.
A használt JS könyvtárak
A két kiemelt szakasz a paraméteres metódushívás. Engedélyezett módban (true) jóval érthetőbb lesz
a form definíció:
7.5 Aszinkron üzem, AJAX - Ajax helperek demó 1-192
Nézzünk meg egy komplexebb megvalósítást az előbb megismert helperek és példák bővítésén
keresztül. A cél a következő oldal lenne:
Felül az „Ügyfelek listája” melletti "nagyon" pontos idő mutatja, hogy a fő oldal mikor töltődött le. Ezzel
nyomon tudjuk követni, hogy tényleg ajaxos, részleges oldalletöltés történt-e. Mert ha igen ez a dátum
nem változhat. Alatta két kereső mező, amikbe a beírt érték az oszlop tartalma alapján fog szűrési
feltételt képezni úgy, hogy az adott oszlop szövegeiben megtalálható a szövegrészlet vagy nem. A
„Keress” gomb megnyomására indul a keresés. Ennek folyamatáról a gomb mellett megjelenő zöld
„Keresés...” felirat tájékoztat, ami a képen nem látszik. Ez a háttérben zajló hosszadalmas keresési
műveletről tájékoztat. A hosszadalmasságot szálvárakoztatással (Sleep) szimuláljuk. Megjelennek a
sorok, ha sikeres a keresés. A képen látható piros „Nincs találat” jelzi, hogy nincs egy sor sem, ami a
feltételnek megfelelt volna. Ekkor az előző keresési lista megmarad. A keresési lista első oszlopában a
jobbra nyíl egy kattintható gomb, amivel a kiválasztott „vásárló” termékeinek alsó listája jeleníthető
meg „Cikkszám”, „Termék név” és „Mennyiség” oszlopokkal. Az Ügyfelek lista jobb szélső oszlopában
vannak a „Részletek” és a „Szerkesztés” linkek, amik a demóalkalmazásban egy előugró modális
ablakban teszik elérhetővé a részletes nézetet és a szerkesztői oldalt.
Kell egy szűrhető ügyfelek listája. A szűrési műveletet, mivel kitöltött input mezők tartalmát
kell felhasználni, célszerű egy HTML formba helyezni. A form kezelését bízzuk az
Ajax.BeginForm-ra. A visszakapott tartalmat, a komplett ügyfelek listát pedig helyezzük ki egy
HTML div-be.
A keresési művelet beindulását, sikerességét és hibáit felügyelni kell a piros és zöld színű
üzenetek miatt. Erre megfelelőek lesznek az AjaxOptions csomag eseménykezelői.
Az ügyfelek listájára kell egy eseménykezelés, ami a bal oldali nyilas oszlopok kattintására
soronként reagál. Ez a változatosság kedvéért ne a beépített MVC ajax helperekkel, hanem a
kézbenntartott, hagyományos jQuery megközelítéssel csináljuk.
o + Az alsó lista feltöltése.
A jobb oldali oszlop "szerkesztés" és "részletek" kezeléséhez egy dialóguskezelő mechanizmust
csinálunk, amit már láttunk pár oldallal előbb az ActionLink tárgyalásánál.
o Közös eseménykezelés a részletek és a szerkesztés dialógus megjelenítéséhez.
o A szerkesztés dialógus ablak validációjának kezelése úgy, hogy a sikeres mentés és
validáció esetén a modális ablak bezáródjon és az Ügyfelek listája frissüljön. Sikertelen
validáció esetén az ablak nyitva marad és megjelennek a validációs üzenetek.
A fő View az Index.cshtml hagyományos módon töltődik be. Az oldal stílusát a következő HTML <head>-
be kerülő CSS csatolás és CSS szakasz biztosítja:
</style>
Ebben a leglényegesebb az első sor, ami importálja a jQuery UI stílusokat, ami az ikonok és a dialógus
ablakhoz kell. A .selector a bal oldali nyilaskiválasztó stílusa lesz, ami miatt az utoljára kiválasztott sor
piros kerettel jelölődik. Az alatta levő definíciók pedig a táblázatnak adnak jobb megjelenítést. Mivel
azonos oszlopértelmű, de két táblázat lesz megjelenítve (egy a kereső mezőknek és alatta az ügyfelek
listájának), az oszlopok szigorúan egységes szélességűre vannak szabva. Emiatt a két egymás alatti
táblázat egységesnek néz ki, mintha egy táblázat lenne. Erre azért van szükség, mert az első táblázat
egy formon belül van, az alatta levőt pedig egy partial View állítja elő és ajax-osan frissül. A következő
kódszakasz a form kezelés, az ügyfelek partial View és a placeholder-ek számára készült:
7.5 Aszinkron üzem, AJAX - Ajax helperek demó 1-194
@{
var ajaxOptions = new AjaxOptions
{
HttpMethod = "Post",
InsertionMode = InsertionMode.Replace,
UpdateTargetId = "updatablelist",
LoadingElementId = "betoltes",
OnComplete = "AttachToSelectorClick",
OnFailure = "SetError",
OnBegin = "ClearErrors",
};
}
<tr id="kereso">
<th></th>
<th>@Html.TextBox("findName")</th>
<th>@Html.TextBox("findAddress")</th>
<th>
<input type="submit" value="Keress">
<span id="betoltes" style="color:green; display: none;">Keresés...</span>
<span id="kereseshiba" style="color:red"></span>
</th>
</tr>
</table>
}
<div id="updatablelist">
@Html.Partial("IndexListPartial", Model)
</div>
Az ajax form AjaxOptions-je ki van emelve egy razor kódblokkba, hogy átlátható legyen. A benne levő
definíciók eseménykezelőit egy következő kódrészlet tartalmazza majd. Ez után az oldalfelirat
következik a pontos idővel. Az Ajax.BeginForm csak az input mezőket és a keresés gombot öleli körbe.
A két Html.TextBox teljesen hagyományos, nem kell modellhez kötni. A „Keresés…” submit gomb
mellett két nem látható <span>-be van ágyazva a betöltés statikus szövege, és a keresési hibaüzenet
helyőrzője. A keresés gombra kattintva megjelenik a zöld „Keresés…” szöveg, mielőtt a form elküldésre
kerül, majd újra eltűnik, mikor a válasz megérkezik a szerverről. Ehhez a működéshez csak arra van
szükség, hogy az AjaxOptions-ban a LoadingElementId = "betoltes" értéket megadjuk. A megjelenés-
eltűnés majd magától működni fog, nem kell kódot írni hozzá (mert már megírták). Az updatablelist-
be kerül a megjelenő ügyféllista. Ezt első esetben még nem ajax módon töltjük fel, hanem normál
partial View segítségével. Erre azért van így szükség, hogy az első keresés előtt is jelenjen meg valami
kezdeti lista. Ez az adatforrás első 10 elemét tartalmazza majd. A lista a keresés során ajax módon fog
frissülni.
7.5 Aszinkron üzem, AJAX - Ajax helperek demó 1-195
@model IEnumerable<MvcApplication1.Models.TemplateDemoModel>
<table>
<colgroup>
<col class="tablecol1" />
<col class="tablecol2" />
<col class="tablecol3" />
<col class="tablecol4" />
</colgroup>
@foreach (var item in Model)
{
<tr data-itemid="@(item.Id)">
<td>
<div class="selector ui-icon ui-icon-arrow-1-e ui-button"></div>
</td>
<td>
@Html.DisplayFor(modelItem => item.FullName)
</td>
<td>
@Html.DisplayFor(modelItem => item.Address)
</td>
<td>
@Ajax.ActionLink("Részletek", "Details", new { id = item.Id }, new AjaxOptions()
{
HttpMethod = "get",
InsertionMode = InsertionMode.Replace,
OnBegin = "openPopupDialog",
OnComplete = "closePopup",
UpdateTargetId = "popupdiv"
}) |
@Ajax.ActionLink("Szerkesztés", "Edit", new { id = item.Id }, new AjaxOptions()
{
HttpMethod = "get",
InsertionMode = InsertionMode.Replace,
OnBegin = "openPopupDialog",
OnComplete = "closePopup",
UpdateTargetId = "popupdiv"
}
)
</td>
</tr>
}
</table>
<div id="detaillist">
<h3>Válassz a listából!</h3>
</div>
<script type="text/javascript">
$(document).ready(function () {
AttachToSelectorClick();
});
Az AttachToSelectorClick funkció meghívásra kerül még egyszer a keresés után is, mivel a kereső form
AjaxOptions blokkjában ott van az OnComplete = "AttachToSelectorClick" definíció is. Ezzel
előírtuk, hogy a form betöltése után post event-ként hívja meg ezt. A szóban forgó funkcióban
7.5 Aszinkron üzem, AJAX - Ajax helperek demó 1-196
feliratkozunk, „old-school” módon, a selector CSS osztállyal jelzett elemekre. (pedig ez nem olyan szép,
mert ez eseménydeklaráció és CSS stílus is egyben. Csak a rossz példa kedvéért…)
function AttachToSelectorClick() {
$('.selector').on('click', function (event) {
$('.selector.selected').removeClass('selected');
var selbutton = $(this).addClass('selected');
var selid = selbutton.closest("tr").attr('data-itemid');
$("#detaillist").load('@Url.Action("DetailListPartial")', { id: selid });
});
}
19. példakód
A klikkelésre reagálva eltávolítjuk az előző kijelöléseket azzal, hogy levesszük a „selected” class-t a
removeClass metódussal minden selector osztályú HTML elemről, amik egyben selected osztállyal is
rendelkeznek. Ilyen elvileg csak egy lehet, de így a legbiztosabb. A jQuery szelektor lehetett volna
div.selector.selected is. Folytatva a kódot: az aktuálisan klikkelt div elemre - amit a $(this) –el tudunk
jQuery objektummá alakítani - hozzáadjuk a selected class-t. Így piros keretet fog kapni a CSS stílus
miatt. Itt megint kihasználjuk a láncolt metódusok előnyét, mert az addClass metódus visszatérési
értéke még mindig a klikkelt jQuery objektummá alakított div elem. Ezt továbbgörgetve megszerezzük
a div tr szülőjét, mert arra generáltunk egy data-itemid attribútumot, ami az aktuális ügyfél Id-jét
tartalmazza. Erre lesz szükségünk. A definíciója ez volt:
<tr data-itemid="@(item.Id)">
<td>
<div class="selector ui-icon ui-icon-arrow-1-e ui-button"></div>
A funkció utolsó sora a #detaillist azonosítóval rendelkező div-be tölti a DetailListPartial action által
generált tartalmat. Az action számára szükséges id paramétert az előzőleg megszerzett selid
tartalmazza. A keresés form AjaxOptions meghatározott két eseménykezelőt:
OnFailure = "SetError",
OnBegin = "ClearErrors",
A ClearErrors funkció a form küldés előtt kiüríti a #kereseshiba div belső tartalmát, hogy ne legyen ott
előző üzenet. Hiba esetén a SetError funkció a hibaüzenetet beletölti az előbb említett div-be és 5x
„megvillogtatja” az egész kereső sort, hogy gond van. A „lüktető” villogást a fadeTo metódusok
egymásba láncolása okozza. A paraméterük az az átlátszatlansági érték, amit el kívánunk érni. Az 1.0 a
normálállapot, a 0.5 a félig áttetsző.
function ClearErrors() {
$('#kereseshiba').html('');
}
function SetError(value) {
$('#kereseshiba').html(value.responseText);
var row = $('#kereso');
for (i = 0; i < 5; i++) {
row.fadeTo('fast', 0.5).fadeTo('fast', 1.0);
}
}
20. példakód
7.5 Aszinkron üzem, AJAX - Ajax helperek demó 1-197
function openPopupDialog() {
$('#betoltespopup').dialog({
autoOpen: true,
width: 600,
height: 100,
modal: true,
resizable: false,
hide: {
effect: "blind",
duration: 300
},
close: function() {
$('#popupdiv').dialog({
autoOpen: true,
width: 600,
height: 'auto',
modal: true,
show: {
effect: "blind",
duration: 300
}
});
}
});
}
function closePopup() {
$('#betoltespopup').dialog("close");
}
A következő két funkció kezeli a szerkesztés dialógus ablak ajax form eseményeit.
OnBegin = "popupValidate",
OnSuccess = "successPopup",
function popupValidate() {
return $('form').validate().form();
}
function successPopup(s) {
if (!s || s.length === 0) {
$('#popupdiv').dialog('close');
$('form#adfrom').submit(); //oldal ujratöltése
}
}
</script>
Az Edit dialógus form submit előtt még lefut egy kliens oldali validáció az OnBegin-ben megadott
popupValidate funkcióban. Ha sikeres a form beküldése és nincs validációs hiba, akkor az Edit ablak
bezárásra kerül a successPopup-ban, továbbá az ügyfelek listája feletti kereső form submit következik,
amivel frissül az alatta levő lista. Így láthatóvá válik a szerkesztésünk eredménye.
@model MvcApplication1.Models.TemplateDemoModel
@{
Layout = null;
var ajaxOptions = new AjaxOptions()
{
HttpMethod = "post",
UpdateTargetId = "editinner",
Confirm = "Biztos, hogy mented?",
OnBegin = "popupValidate",
OnSuccess = "successPopup",
};
}
<div id="editinner">
@using (Ajax.BeginForm("Edit", ajaxOptions))
{
@Html.ValidationSummary(true)
<fieldset>
<legend>TemplateDemoModel</legend>
<p>
<input type="submit" value="Mentés" />
</p>
</fieldset>
}
</div>
21. példakód
Az ügyfél termékeit generáló DetailListPartial.cshtml is annyira egyszerű, hogy csak egy táblázatot
generál ezért nem érdemes ide másolni. Következzen az egészet menedzselő kontroller, szintén
szakaszolva:
A LongTimeDBAccess szimulálja, mintha nagyon nagy feladatot kéne végezni. Csak azért van, hogy
látszódjanak az ajax eseményei. Az Index action szolgáltatja a kezdeti ügyféllistát 10 sorral. (és generál
100 demó ügyfelet)
[HttpPost]
public ActionResult IndexListPartial(string findName, string findAddress)
{
7.5 Aszinkron üzem, AJAX - Ajax helperek demó 1-199
this.LongTimeDBAccess();
bool filtered = false;
var query = TemplateDemoModel.GetList();
if (!string.IsNullOrEmpty(findName))
{
query = query.Where(l => l.FullName.Contains(findName));
filtered = true;
}
if (!string.IsNullOrEmpty(findAddress))
{
query = query.Where(l => l.Address.Contains(findAddress));
filtered = true;
}
this.SendNotFount();
return null;
}
return PartialView(TemplateDemoModel.GetList().Take(10).ToList());
}
A további action metódusok az ügyféllista részletek és szerkesztés dialógus ablakait szolgálják ki,
teljesen hagyományos módon.
[HttpPost]
public ActionResult Edit(int id, FormCollection coll)
{
var model = TemplateDemoModel.GetModell(id);
if (this.TryUpdateModel(model))
return new EmptyResult();
return PartialView(model);
}
[HttpPost]
public ActionResult DetailListPartial(int id)
{
var item = TemplateDemoModel.GetModell(id);
return PartialView(item.PurchasesList);
}
}
24. példakód
7.5 Aszinkron üzem, AJAX - Ajax helperek demó 1-200
A DetailListPartial a vásárlások listáját állítja elő, a bejövő id alapján. Emlékeztetőnek ezt az id-t a 19.
példakód JS kódjában állítjuk elő, akkor amikor a jQuery load() metódusával kérjük le ettől az action
metódustól a tartalmat.
Ez a demó így leírva 8 oldalt tett ki és ráadásul jó összetett. Azonban az eddig látottak alapos
összegzésére is éppen csak elégséges. Az AJAX témakör nagy terület, ezért csak arra tettem egy
próbálkozást, hogy az MVC által nyújtott segítségről lerántsam a leplet. Az interneten számos
bemutató és tipp van arra nézve, hogy más JS keretrendszereket hogyan lehet felhasználni az MVC
keretrendszerrel. Érdemes kalandozni egy kicsit, hogy milyen ötletek és néha tényleg nagyszerű
megvalósítások láttak már napvilágot.
7.6 Aszinkron üzem, AJAX - JSON adatcsere 1-201
Az előző példában az MVC beépített Ajax helpereivel kapcsolatban láttuk, hogy a HTML tartalom
hogyan bővíthető, cserélhető AJAX módon. Ezek közös jellemzője volt, hogy a szerverről kapott, partial
View szerint renderelődött HTML darabot beillesztettük a böngészőben már ott levő teljes oldalba. Így
a teljes HTML oldalba beágyazott kis szakaszok tartalmát frissítettük. Ez az oldalrészlet letöltögetés,
mint láttuk teljesen jó, mert kis adatmennyiséget és nagyobb sebességet jelentett. Azonban előfordul
sok olyan helyzet, amikor még ennyi adatra sincs szükség. Például, ha egy táblázatnak csak egy cellája
változott meg, akkor nem biztos, hogy érdemes az egész táblázatot újra letölteni. Egy másik helyzet
lehet, amikor egy legördülő lista elemeit kell feltölteni egy másik legördülő lista kiválasztott értéke
szerint. Ezek a tipikus 'főcsoport – csoport – alcsoport – termék' logikailag összefüggő combobox
csoportok. Így szokták megoldani, ha az a cél, hogy ne töltődjön le a legutolsó (pl. termék) combobox-
ba a több tízezres terméklista. Az egyre szűkülő szűrést a combobox-ok egymás utáni kiválasztott
értékei biztosítják, így az utolsó (termék) combobox csak kevés elemet fog tartalmazni. Egy másik
nagyon jellemző alacsony adatigényű helyzet, amikor a szabadon kitölthető textbox mint kereső mező
funkcionál úgy, hogy a begépelt karakterek szerint egy lista jelenik meg a lehetséges értékekről. Ezek
az un. autocomplete textbox-ok. Az MVC beépítetten nem ad támogatást arra, hogy ilyen textboxot
csak úgy odadobjunk a View-ra és használjuk, nincs rá Ajax helper. Amire támogatást nyújt az a háttér
adatcseréhez szükséges JSON adatok kezelése.
Az előbbi Ajax helper demó egy jelentősen leegyszerűsített változatán keresztül fogunk megnézni egy
lehetséges megvalósítását az autocomplete textbox-nak. Az a funkcionalitás lenne a végcél, hogy a
kereső mezőkbe begépelt néhány karakter után megjelenjen egy lista a keresőmezőhöz tartozó oszlop
tartalma alapján, szűrve a begépelt karakterekkel. A képen láthatóan a '22' szótöredékkel rendelkező
sorok jelenjenek meg.
36
http://jqueryui.com/autocomplete/. API dokumentáció: http://api.jqueryui.com/autocomplete/
7.6 Aszinkron üzem, AJAX - JSON adatcsere 1-202
Az AJAX demóban szereplő JS kódot át kell alakítani, hogy az autocomplete metódussal be tudjuk
indítani a képességeket.
<script type="text/javascript">
$(document).ready(function () {
AttachToSelectorClick();
$("input[data-completefield]").each(function () {
var textbox = $(this);
textbox.autocomplete({
minLength: 2,
source: function (request, response) {
$.getJSON("@Url.Action("AutoComplete")", {
term: request.term,
field: textbox.attr("data-completefield")
}, response);
},
}); //end autocomplete
A View kódján csak minimálisan kell módosítani: a két Html helperes textbox-ot innentől már érdemes
lecserélni hagyományos input mezőre, mivel úgysem hordoznak többé modellel kapcsolatos értékeket.
[HttpGet]
public ActionResult AutoComplete(string term, string field)
{
if (string.IsNullOrEmpty(term)) return Json(null);
var query = TemplateDemoModel.GetList();
IEnumerable<string> response = null;
switch (field)
{
case "findName":
response = query.Where(l => l.FullName.Contains(term)).Select(l => l.FullName);
break;
case "findAddress":
response = query.Where(l => l.Address.Contains(term)).Select(l => l.Address);
break;
}
return Json(response, JsonRequestBehavior.AllowGet);
}
A két paramétere név szerint megegyezik a getJSON által szolgáltatott paraméterekkel. A „term”-be
érkeznek a gépelt karakterek és a field alapján dől el, hogy végeredményben melyik textbox által
képviselt adatmező alapján kell a listát szolgáltatni. A lista egyszerű szöveges lista, jelen esetben egy
felsorolás. A felsorolásból a kontroller Json metódusa készít JSON tartalmú response adatot. A Json()
metódus egy JsonResult objektumot gyárt le. Így a visszatérési ActionResult-ot szolgáltató kód
lehetne ez is:
Az action GET-re reagál, de ahhoz hogy JSON adatot szolgáltasson, GET request alatt engedélyezni
kell a JsonRequestBehavior.AllowGet paraméterrel . Ez egy kis biztonsági rés, mert így az actionünket
más weboldalak is fel tudják használni. Normál esetben ajánlatos a post használata. Mit kell tenni,
hogy ne GET legyen a HTTP request metódusa? Mindössze le kell cserélni a $.getJSON-t $.post-ra a
javascript-ben.
$.post("@Url.Action("AutoCompletePost")", {
term: request.term,
field: textbox.attr("data-completefield")
},
response);
Az action nevét megváltoztattam, hogy a példakódban meglegyen mind a két (get és post) action is.
Az action metódusban a változás mindössze a HttpPost attribútumban és a Json paraméterében van:
[HttpPost]
public ActionResult AutoCompletePost(string term, string field)
{
//A listát összeállító kód...
return Json(response, JsonRequestBehavior.DenyGet);
}
37
Elvileg nem lenne szükség erre az attribútumra, mert a name is használható lenne. Csak a demó kedvéért…
7.6 Aszinkron üzem, AJAX - JSON adatcsere 1-204
a fontos, a JSON adattovábbítás. Nézzük meg, hogyan zajlik ez a háttérben egy böngészőben, ami
képes listázni a HTTP forgalmat. Amint beírok két karaktert a textboxba elindul a post lekérés.
Chrome böngészőben a developer/debugger ablakban (press F12) az alábbi hálózati esemény jelent
meg nálam a listában:
Az URL az actionre hivatkozik. A metódus POST és a kezdeményező (Initiator) a jQuery JS kód volt.
Rákattintva a sorra megjelennek a részletek:
A request HTTP fejlécben ott van a lényeg. A fontos adatok piros nyíllal vannak jelölve. A request URL
és a form adatok:
A Form Data egy kicsit félreérthető ebben az esetben, mert nem form submit történt, és nem a
kereső form került elküldésre, hanem a $.post metódusban megadott adatok jelentek itt meg.
Picit foglalkozzunk a JsonResult objektummal. Ez az MVC beépített szolgáltatása arra, hogy normál C#
objektumot JSON-á, azaz szöveges javascript objektummá alakítsunk. Két paraméterének már láttuk
a hatását: a Data-nak, ami egy objektum és a JsonRequestBehavior-nak, ami egy biztonsági kapcsoló.
RecursionLimit – A Data objektum lehet egy osztály, további osztály típusú propertykkel.
Ennek a fa struktúrának a maximális bejárási mélységét adhatjuk meg. A bejárás eredménye
egy hasonló felépítésű javascript objektum lesz. A default értéke: 100.
A probléma, mint mindig, itt is a dátummal van, mert a JS-ben nincs beépített dátum alaptípus. Ezért
a JSON szöveggé sorosítás eredményében egy DateTime érték így jelenik meg:
{ "CurrentTime":"\/Date(1367774070488)\/" }
Ez olyan, mint egy Date objektum konstruktora, aminek a paramétere a Unix alapdátumtól39 eltelt idő
ezredmásodpercekben számolva. Az egyik dolog, amit tehetünk, hogy a DateTime propertyket
szöveggé alakítjuk a szerver oldalon. A másik, hogy a kliens oldalon ezt valahogy feldolgozzuk. A
következő kódokban ezt fogjuk megtenni, és a JSON kezelés további aspektusaira is láthatunk példákat.
38
http://msdn.microsoft.com/en-us/library/system.web.script.serialization.javascriptserializer.aspx
39
1970.01.01 00:00:00
7.6 Aszinkron üzem, AJAX - JSON adatcsere 1-206
A ServerData.cshtml View kódja két linkkel kezdődik. Az első egyszerűen meghívja a GetServerData
actiont és ezzel megmutatkozik a nyers JSON válasz a megjelenő oldalon.
{"Message":"Szerver idő","CurrentTime":"\/Date(1367778850927)\/","EniacFinished":"\/Date(-
753584400000)\/"}
A második link egy AJAX hívással hívja ugyanezt az actiont. A kicsi trükk, hogy nincs szükség
UpdateTargetId-re, mert nem HTML darabot akarunk injektálni az oldalba. Az AJAX hívás végén
(OnSuccess) értékadás miatt meghívásra kerül a ParseJson funkció. A táblázat csak adatkijelzésre
szolgál. A cellák id attribútumokkal vannak ellátva.
<table>
<colgroup>
<col style="width:150px"/>
<col style="width:150px"/>
<col style="width:150px"/>
</colgroup>
<tr>
<th> Adat név </th><th> Normál JSON </th><th> Parsolt JSON </th>
</tr>
<tr>
<td> Szöveg </td><td id="szoveg">...</td><td id="szovegph">...</td>
</tr>
<tr>
<td> Szerver idő </td><td id="szerverido">...</td><td id="szerveridoph">...</td>
</tr>
<tr>
<td> Eniac elkészült </td><td id="eniacfinish">...</td><td id="eniacfinishph">...</td>
</tr>
</table>
A ParseJson funkció három paramétert is fogad. Az első a beérkező adat, ami az előző példákban a
HTML darabka tartalma lett volna. Most azonban ez egy valódi javascript objektum. Ez, a jQuery egy
szolgáltatása. Ha a szervertől érkező válasz application/json típusú, akkor azt egyből parsolja is és ez
7.6 Aszinkron üzem, AJAX - JSON adatcsere 1-207
vehető át paraméterként. Ennek megfelelően a Json objektum tulajdonságai név szerint egyeznek az
action metódus anonymous objektum propertyjeivel. A $(#idNév).html(json.tulajdonság) sorok
feltöltik az idNév-nek megfelelő tábla cellákat. A JSON parsolást manuálisan is megtehetjük a
parseJSON metódussal. Erre itt most nincs igazából szükség, csak a demó miatt van ott. A parsed
változó helyett lehetett volna használni a Json paramétert is.
<script type="text/javascript">
function ParseJson(json, status, ajaxXHR ) {
$('#szoveg').html(json.Message);
$('#szerverido').html(json.CurrentTime);
$('#eniacfinish').html(json.EniacFinished);
$('#szovegph').html(parsed.Message);
$('#szerveridoph').html(ParseDate(parsed.CurrentTime));
$('#eniacfinishph').html(ParseDate(parsed.EniacFinished));
}
function ParseDate(value) {
var pdate = new Date(parseInt(value.substr(6)));
return pdate.toLocaleString();
}
</script>
@section Scripts {
@Scripts.Render("~/bundles/jqueryval")
@Scripts.Render("~/bundles/jqueryui")
}
A futás eredménye:
A DateTime problémára létezik egy harmadik megoldás is, mégpedig az, hogy lecseréljük a
JavaScriptSerializer alá bedolgozó konvertert egy olyanra, amit a .NET beépített DateTime típusát
megfelelő formára hozza. Ezzel a módszerrel bármilyen saját típus számára is megadhatunk konvertert.
Kezd olyanná válni az MVC felfedezése, mint egy rókavadászat40, mert megint találtunk egy bővítési
pontot (jeladót). Kezdjük is el a formára hozást!
Szükségünk van tehát egy konverterre, amit a JavaScriptConverter absztrakt osztályból kell
származtatni:
40
Rádióamatőr tájékozódási futás. http://wiki.ham.hu/index.php/Rókavadászat
7.6 Aszinkron üzem, AJAX - JSON adatcsere 1-208
DateTime datet;
if (DateTime.TryParse(dateString, out datet))
return datet;
return null;
}
}
A Serialize metódus egy dictionaryt vár visszatérési értékként, aminek név-érték párjai jelennek majd
meg a JS objektumban, mint 'tulajdonság:érték' párok. A keletkező JS objektum JSON alakja:
Érdemes implementálni a Deserialize metódust is, mert a model binder értelmezni tudja a JSON
tartalmat és ez még hasznos lehet, ha visszafelé küldjük a szervernek a DateTime adatunkat.
A JS kódban látható, hogy a CurrentTime innentől egy objektum és nem szöveg, mert megvannak a
dateTime, date és long tulajdonságai, úgy ahogy azt a DateTimeJsonConverter-ben összeállítottuk. A
működésének eredménye:
Ezzel a módszerrel írhatunk bármelyik saját modell típusunkhoz olyan egyedi sorosítót, amilyet
akarunk. Kiemelhetjük a modellosztályunk lényeges propertyjeit,és egyedi típuskonverziókat
hozhatunk létre. Ezzel flexibilisebb megoldást adhatunk annál, minthogy kizárjuk a
ScriptIgnoreAttribute attribútummal a nem szükséges propertyket.
Létezik egy negyedik, elkerülendő megoldás is. Az, hogy a beérkező ”/Date(1367778850927)/” gyári
szöveget a ’/’ jelektől lecsupaszítva odaadjuk a javascript eval() funkciónak. De ez nagyon veszélyes
lehet. Úgy hallottam, hogy az eval() használatának számos weboldal és felhasználói adat esett már
áldozatul…
7.6 Aszinkron üzem, AJAX - JSON adatcsere 1-210
Az előzőekben a JSON adatok csak egy irányban, a szerver felől érkeztek. Következzen az a rész, amikor
a böngészőben levő JSON adatot a szervernek küldjük el.
Először is nézzünk egy nagyon egyszerű példát arra, hogyan lehet egy action elérhetőségét úgy
biztosítani, hogy csak AJAX módon lehessen meghívni POST HTTP metódussal.
Ezzel levédhetjük az actionünket a hagyományos eléréstől. A példa egyszerű, de amire a figyelmet fel
szeretném hívni, az a Request objektumban létező IsAjaxRequest() metódus. Ez true-t ad vissza AJAX
hívás esetén. A JSON adatküldés kipróbáláshoz egy kicsike actionre lesz csak szükség, ami a bejövő
modellben a ClientUTCTime-ot megnöveli 1200 nappal, majd módosítás után visszapasszolja az
újdonsült MyJsonResult segítségével.
[AjaxPost]
public ActionResult SetServerData(MyJsonModell modell)
{
modell.Id++;
modell.ClientUTCTime = modell.ClientUTCTime.AddDays(1200);
return new MyJsonResult(modell);
}
A View ezzel foglalkozó szelete egy táblázat, hogy legyen hova írni az eredményeket:
function SendJsonData() {
var MyJsonModell = {
Id: 1,
Message : 'Küldött',
ClientUTCTime: new Date().toUTCString(),
Internal: {
Id: 1001,
Message: 'Küldött internal',
ClientUTCTime: new Date().toUTCString(),
}
};
$('#sendId').html(MyJsonModell.Id);
$('#sendIdo').html(MyJsonModell.ClientUTCTime);
$.ajax({
url: '@Url.Action("SetServerData")',
data: JSON.stringify(MyJsonModell),
dataType: "json",
contentType: 'application/json; charset=utf-8',
async: false,
7.7 Aszinkron üzem, AJAX - Az MVVM keretrendszerekről néhány szóban 1-212
type: "POST",
error: function (jqXHR, textStatus, errorThrown) {
alert(jqXHR + "-" + textStatus + "-" + errorThrown);
},
success: function (json, status, ajaxXHR) {
$('#recId').html(json.Id);
$('#recIdo').html(json.ClientUTCTime.dateTime);
}
}
);
}
Ebben a funkcióban egy JS objektum kitöltésre kerül, majd az aktuális értékei a táblázat első oszlopába,
mint kiinduló adatok. A jQuery.ajax metódussal összeállítunk egy JSON POST requestet, ami az
actionhöz küldi az MyJsonModell tartalmát. A JS oldalon a sorosításról a JSON.stringify() metódus
gondoskodik. Az actionbe paraméterként megérkezik a modell. Ez most számunkra a lényeg. Ugyanis
a .Net-es MyJsonModell fel lesz töltve azokkal az adatokkal, amit a JS kódban összeállítottunk és
sorosítottunk. Még az Internal property és a dátum adatok is helyesen lesznek kitöltve. Erről is a model
binder gondoskodik, úgy hogy az objektum feltöltését a JavaScriptSerializer-el, és annak is a
Deserialize() metódusával végzi el. A model binder csak akkor fogja így feltölteni, ha a request tartalom
típusa JSON (a contentType: 'application/json’).
A kód futását folytatva, az actionből visszacsorog egy JSON csomag, amit a már megismert módon, a
json paraméterben vehetünk át (mellette még a requestre kapott válasz státuszát is). Ennek két értékét
is kiírjuk a táblázat második oszlopába. A dateTime értékét még mindig a MyJsonResult biztosítja.
A JSON adat csomag összeállításról még idekívánkozik, hogy az előbb bemutatott lehetőséget a
beépített Ajax helperek nem támogatják. Ezért kellett közvetlenül használni a jQuery.ajax metódusát.
Normál form elemek küldésénél (Ajax.BeginForm) és az AJAX linknél (Ajax.ActionLink) egyébként a
háttérben szintén ezt a jQuery.ajax metódust dolgoztatják. A beépített Ajax helperek nem is csinálnak
nagyon mást, minthogy ennek a paramétereit összeállítják az unobtrusive attribútumokból.
Az eddig átnézett AJAX adatkezelésben volt néhány olyan pont, ami gondolkodásra indíthat. Odáig
eljutottunk, hogy a küldésre kijelölt adatokat a HTML elemek data-* attribútumaiba tároltuk. Azok,
mint metaadatok befolyásolták a műveleteket. Nem nagyon használtuk az on… -al kezdődő
eseményeket, mert így jobban el lett választva a kód a HTML markuptól. A gond akkor kezdődött,
amikor a táblázatot úgy töltöttük ki, hogy megkerestük a HTML elemet Id alapján, majd a .html jQuery
metódussal beleírtuk az adatot. A JSON küldésénél azt is megtettük, hogy ezeket az adatokat egy JS
objektumból szedtük ki, majd gyakorlatilag ez az objektum került kiküldésre a szerver felé. Ebben a
folyamatban a kulcs szereplők:
A helyzet az, hogy ahhoz, hogy elérjük azt a néhány fő célt, hogy a továbbított adat kicsi (JSON) és az
oldal tartalma a lehető legdinamikusabb legyen, az előbbi felsorolás összes jellemzőjét használnunk
kell. A mi kis példánkon ezek kezelése még a tűréshatáron belül van, mert pici modellt néhány HTML
elemmel kapcsolatban használtunk. Azonban ahogy nő a kezelendő HTML elemek száma és a JS-ben
megjelenő modell mérete, borzalmas munkának és áttekinthetetlen kódnak nézünk elébe. Ennek
felismerése folyamán elindult több JS keretrendszer fejlesztése, amik ezt a munkát hivatottak levenni
a vállunkról. Az előbbi felsorolás által vázolt területekre nagyon jól illeszthető a WPF/Silverlight
technológiákban is bevált MVVM minta, amit szintén a felhasználói interfész megalkotása számára
nyújt előnyöket. Ezzel kapcsolatban, négy szereplőt lehet elkülöníteni, amik összeegyeztethetőek az
előbb vázolt listával is:
M(odel) – Az adatmodell, ami a kliens oldali strukturált tárolást végzi. A JS oldalon összeálló
modellnek, vagy tükörképének meg kell jelennie a szerver oldalon, másként csak kliens oldali
homokozónk lenne.
V(iew) - A nézet. Ez a megjelenítés összessége. Itt most a HTML + CSS. A HTML sablonját az MVC
View tudja szolgáltatni, de ez nagyon egyszerű, szinte statikus oldal, így szerver oldalon nagyon
kevés dinamikus oldalgenerálásra van szükség.
VM – A ViewModel feladata, hogy a M(odel) és V(iew) között kapcsolatot tartson fenn. Adatkötés,
transzformáció, eseménykezelés a feladatköre. Nagyon sok jellemzője deklaratívan kerülhet
meghatározásra.
Szolgáltatás kapcsolattartó, amolyan kontroller. Ennek nem maradt betű az MVVM-ből, pedig ott
lesz minden kódban. Ez reagál a submit jellegű adatfeltöltésre és az új oldal betöltésekre, és elvégzi
a kezdeti beállításokat.
Számos ilyen keretrendszer érhető el, némelyik komplett termék, kliens és szerver oldali framework
párral, mások csak a kliens oldali megvalósítást tartalmazzák. Szintén választóvonal közöttük, hogy
igényelnek-e további keretrendszert, vagy saját megoldásuk van a DOM kezelésére. Ugyan nem
mindnek tisztán az MVVM minta megvalósítása a célja, de a tipikus AJAX problémákra mind
alkalmazható.
Még további 20-at listázhattam volna, de ezek tűnnek számomra ígéretesnek. Az Angularjs egy Google
termék, tehát biztosan jó támogatottsága van és lesz. A Backbone.js szintén gyakran használt kiegészítő
7.7 Aszinkron üzem, AJAX - Az MVVM keretrendszerekről néhány szóban 1-214
az ASP.NET MVC fejlesztők körében. A Kendo UI (Telerik) érdekessége, hogy a szerver oldali MVC
helpereket is, és egyéb eszközöket is elkészítették, viszont ezért sok pénzt kérnek, de sokat is adnak. A
Knockout.js azért figyelemre méltó, mert az MVC Internet projekt template odavarázsolja a generált
projektünkbe. ASP.NET környezetben nagyon nagy a népszerűsége (gondolom emiatt is). A
Knockoutmvc különlegessége, hogy MIT licenc alatt használható kliens, MVC támogatással, GitHub-on
elérhető forrással. Érdemes körülnézni, melyiknek mi az előnye, mielőtt elköteleződnénk az egyik
mellett.
Nagyjából ennyi kitekintést szerettem volna tenni, amit úgy gondolom, hogy az AJAX-MVC
konstellációban mindenképpen érdemes említeni. A tendencia az ASP.NET WebApi megjelenése óta
afelé tolódott el, hogy az erősen AJAX centrikus oldalak kiszolgálása nem igényli számottevően az MVC
képességeit. Amint utaltam is rá, egy MVVM felépítésű kliens alig igényel szerver oldali HTML
renderelést. Számos esetben a letöltött oldal egy statikus HTML, felcímkézve attribútumok
sokaságával.
7.7 A model binder - Az MVVM keretrendszerekről néhány szóban 1-215
8. A model binder
Az előző fejezetekben megnéztük a Modell + View + Kontroller főszereplőket. Az eddigiek alapján fel
tudunk építeni egy AJAX támogatottságú teljes web site-ot. Láttuk hogyan kerül át modell vagy csak a
ViewData formájában a kontrollerben összeállított adat a View-ba. Most azt a szegmensét nézzük meg
a működésnek, ami a böngészőtől érkező request adatfeldolgozásával foglalkozik. Eddig számos helyen
utaltam rá, hogy létezik ez a mechanizmus, ami az érkező adatokat képes értelmezni és ezeket
különféle szempontok szerint, típusosan továbbadni a kontroller actionje számára. Az MVC alapokon
túllépve nélkülözhetetlen ennek a megértése. Eddig nem sokat kellett vele foglalkozni, mivel úgy is
teszi a dolgát és az esetek jó részében (mondjuk 90%-ban) teljesen megfelelőek a beépített
képességek. Mikor azonban az ötleteink, igényeink és ezzel az alkalmazásaink is elkezdenek bővülni,
előbb-utóbb elérkezik a pont, amikor a háttérben (csendben) dolgozó rendszer kevésnek bizonyul.
Ekkor kénytelenek leszünk hozzányúlni a model binder-hez.
A komplexitás miatt ezt a témát három részletben nézzük át. Az első két részben a normál működését.
Utána egy a komolyabb szakaszban megnézzük a belső működését és annak néhány csapdáját is. Ehhez
a fejezethez is készült egy saját modell: CategoryModel néven:
[Required]
public string WillNeverValid { get; set; }
//Lehetőleg ne használjunk Action és Controller nevű tulajdonságokat, most is csak a demó miatt…
//public string Action { get; set; }
//public string Controller { get; set; }
[Display(Name = "Alkategóriák")]
public List<CategoryModel> SubCategories { get; set; }
Kezdjük ott, hogy a request megérkezik szerverre és az ASP.NET + MVC összeállítja a Request
objektumot.
A POST method esetében lehetőségünk van szintén URL paramétereket átadni az action
HTML attribútumon keresztül, de megkapjuk a HTML form input mezőit és értéküket is.
Eddig a pontig minden beérkező adat kizárólag string típusú „név”-”érték” formában áll rendelkezésre
egy szótárban. Ez nem típusos és nem passzol a modellünk (általában) fa jellegű felépítéséhez. A
következő kódrészlet bemutatja, hogy milyen lenne az élet model binder nélkül, ha a request
dictionaryből kéne kimazsolázni az input mezők tartalmát, némi validációval megtoldva:
[HttpPost]
public ActionResult Edit()
{
int id;
DateTime createdDate;
return RedirectToAction("Index");
}
Lehetőségünk van még a FormCollection átvételére/igénylésére, így kicsit átláthatóbb kódot kapunk.
A FormCollection egy kivonat, ami tartalmazza a Request objektumból azokat a bejegyzéseket,
amelyek a post során a HTML formhoz tartozó input mezők alapján érkeztek (Request.Form[]). Szintén
string alapú név-érték párokból áll, de több segédmetódus áll rendelkezésünkre, az adatok elérésére.
Az egyik ilyen a GetValue(”név”), ami egy ValueProviderResult-al tér vissza. Ez pedig képes arra, hogy
az igényelt típusra konvertálja a FormCollection-ból kinyert elemet.
8.1 A model binder - Egyszerű típusok és a beépített lehetőségek 1-217
[HttpPost]
public ActionResult Edit1(int id, FormCollection coll)
{
try
{
var model = CategoryModel.GetCategory(id);
ValueProviderResult fullname = coll.GetValue("FullName");
ValueProviderResult createdDate = coll.GetValue("CreatedDate");
model.FullName = (string)fullname.ConvertTo(typeof(string));
model.CreatedDate = (DateTime)createdDate.ConvertTo(typeof(DateTime));
}
catch (Exception ex)
{
return Content(ex.Message);
}
return RedirectToAction("Index");
}
Egy fokkal jobb, de még mindig sok a munka vele és egyáltalán nem generikus megoldás, hasonlóan az
előző „request bányász” megközelítéshez. Néhány előnye viszont van:
Ez azért lényeges, mert a model binder megpróbál majd minden tőle telhetőt, hogy segítségünkre
legyen, de ennek ára van a sebességben, pontosságban és az biztonságban. A FormCollection az MVC
első verziója óta rendelkezésre áll, manapság nem igazán van használatban. Személy szerint néha
hibakeresés esetén szoktam még igénybe venni, mert az input mezők meglétét és tartalmát kicsit
egyszerűbb ellenőrizni, mintha a Request gyűjteményét kellene vizsgálgatni. A másik hasonló ok, ami
miatt érdemes lehet használni, hogy nem a hagyományos (default) model binder-t használja, tehát ha
ez utóbbival gondok lennének a FormCollection még jól jöhet.
Ahogy az eddigiekből is kitűnik, három feladatot kell elvégezni, a bejövő nyers request adatokkal:
A model binder is ezeket a műveleteket végzi el, ha megkérjük rá. Ahogy láttuk a model binder két
esetben indul el. Akkor, ha egyáltalán nem kérünk metódusparamétert és magunk indítjuk az
UpdateModel, TryUpdateModel, ValidateModel vagy a TryValidateModel metódusokkal. A másik eset,
amikor az actionünk típusos paramétert vagy paramétereket vár. A paraméterek lehetnek primitív
típusok és kollekciók, valamint egy tipikus MVC modell, azaz komplex típus, osztály is. Fontos tudni,
hogyha a route bejegyzésben egy beérkező értéket nevesítünk az URL-ben, mint például az Id-t, akkor
azt is a model binder fogja a kért típus szerint előállítani (jellemzően: int Id). A következő példa
tartalmazza ezt az id-t és a modell propertyjeinek megfelelő nevűeket a paraméterlistában.
[HttpPost]
public ActionResult Edit2(int id, string FuLlNAme, string Fullname, string fullname,
DateTime createdDate, List<CategoryModel> subCategories)
{
var model = CategoryModel.GetCategory(id);
model.FullName = FuLNAme;
model.CreatedDate = createdDate;
model.SubCategories = subCategories;
return RedirectToAction("Index");
}
8.1 A model binder - Egyszerű típusok és a beépített lehetőségek 1-218
A metódus paraméterlistájában a paraméterek nevei nem követik a modell property neveinek kis-
nagybetűs alakját. Ezzel nem foglalkozik a model binder. Annyira figyelmes, hogyha több formában is
leírjuk, az összesbe betölti az értéket, jelen esetben a „FullName” nevű <input> tartalmát.
public ActionResult Edit2(int id, string FuLlNAme, string Fullname, string fullname)
Sőt, ha lehetséges a típuskonverzió, akkor más típusba is igényelhetjük az input mezők értékeit. Az
alábbi metódus szignatúra szerint szintén megfelelő adatok fognak érkezni a createdDate és a kisbetűs
paraméter változatába.
Ez a viselkedés rámutat a model binder egy tulajdonságára is, nevezetesen, hogy a propertyk számára
egyesével keres adatfeltöltési megoldást, ami sebesség problémákat is okozhat. (persze nem ilyen
primitív típusoknál, mint a példában). Modell típusú paraméterrel egy komplett kitöltött modellt
kaphatunk. A lenti példában az inputmodel Id propertyje is beállításara kerül, így nincs szükség az Id-
re, mint metódusparaméterre.
[HttpPost]
public ActionResult Edit3(CategoryModel inputmodel)
{
var model = CategoryModel.GetCategory(inputmodel.Id);
model.FullName = inputmodel.FullName;
model.CreatedDate = inputmodel.CreatedDate;
model.SubCategories = inputmodel.SubCategories;
return RedirectToAction("Index");
}
A „modell, mint paraméter” esetén azonban figyelni kell, hogy a form tartalmaz-e minden olyan
szerkesztő mezőt, aminek a tartalmára számítunk a beérkező modellben. Ez gyakori hiba szokott lenni
és sokszor csak futásidőben derül ki.
Az sincs megtiltva, hogy a komplett modell mellett még a modellen egyébként szereplő property
neveket pluszban elkérjük paraméterekben.
Lehetőségünk van a modell feltöltést a kezünkbe venni. Ennek a régi módszere, hogy nem kérünk
metódus paraméterként adatokat a request alapján csak az Id-t. Persze az Id-re sincs szükségünk
gondolhatnánk, mert kivehetnénk a Requestből. De annak, hogy miért jó az ld-t így kérni, két nyomós
oka is van. Az egyik, hogy valószínűleg a Route bejegyzésben úgy is szerepel, tehát az alkalmazás
logikánk előírja. A másik, hogy az Id ritka esetben igényel típus validációt, legfeljebb null értéket kapunk
(DateTime típusú Id elég ritka…). Az Id validálása általában az alapján történik , hogy a felhasználónak
van-e egyáltalán joga az adott azonosítóval hivatkozott adatot/entitást olvasni vagy módosítani. Ritka
eset, ha a módosítandó modellünknek nincs egyedi azonosítása. Alábbi példában a kontroller
UpdateModel metódusával próbáljuk kitöltetni az adatszolgáltatóból elkért modellt az Id alapján. Az
UpdateModel továbbmegy és a bejövő adatokat a modell property kitöltése előtt megpróbálja
validálni is. Ha a típuskonverzió vagy a validáció sikertelen, akkor InvalidOperationException-t dob,
8.1 A model binder - Egyszerű típusok és a beépített lehetőségek 1-219
amit elkaphatunk a catch blokkban. A validálás alapjai a modell propertykhez kapcsolt attribútumok,
ahogy az a 4.3.2 fejezetben láttuk.
[HttpPost]
public ActionResult Edit4(int id)
{
try
{
var model = CategoryModel.GetCategory(id);
this.UpdateModel(model);
}
catch (Exception ex)
{
return Content(ex.Message);
}
return RedirectToAction("Index");
}
[HttpPost]
public ActionResult Edit5(int id)
{
var model = CategoryModel.GetCategory(id);
if (this.TryUpdateModel(model))
{
//Minden Ok, mehet a mentés
}
else
{
//Validációs hiba -> form újramegjelenítése
}
return RedirectToAction("Index");
}
A normál UpdateModel is az if(TryUpdateModel -t használja belül.
Egyszerű, mint az 1x1. Megfogjuk a modellünket és generáltatunk egy típusos View-t az Add View
varázslással. A View-ba belekerül a modell minden propertyje. A post feldolgozásával a requestből a
model binder kiveszi a formon levő összes <input> mező értékét és feltölti a modell minden egyes
propertyjét név szerint. Biztos, hogy mindig ez történik? Mi van akkor, ha a felhasználónak szeretnék
csinálni egy felületet, ahol kizárólag az email címét változtathatja meg vagy valami hasonló egyszerűt,
aminél nincs szükség a modell összes propertyjére és/vagy az adatforrásban tárolt entitás összes
mezőjére? A felhasználó adatbázisban tárolt adatai legritkább esetben állnak egy Id-ből és egy email
címből. Ott van még a neve, avatárja, időbeli adatai, stb. Legalább három megoldásunk is van az eddig
látottak alapján.
1. Csinálunk egy új modellt, ami csak az Id-t és az email címet tartalmazza. A modellhez
összeklikkelünk egy View-t. Az action pedig csak ezzel a modellel foglalkozik, és ezt frissíti az
adatszolgáltatóba.
2. Nem csinálunk modellt, hanem manuálisan összeállítjuk a View-t. Az Id-t és az email címet
átvesszük az action metódus paramétereiben vagy a FormCollection-ból kivesszük. A Request
bányászatot már nem is említem.
3. Nem csinálunk új modellt, hanem használjuk a komplex ORM alapú modellt az összes
propertyjével.
Az elsővel az a gond, hogyha minden egyes részinformációs blokk esetére csinálni szeretnénk egy új
modellt, akkor a hozzá illeszkedő a validációt és egy adat mappelést propertynként újra és újra meg
kell oldani. Ez akkor kezd problémás lenni, ha az adatbázis ORM modellnek van például 100 propertyje,
de az aktuális oldalon csak 10-et töltetnénk ki a felhasználóval. Ráadásul nagy az esély arra, hogy minél
8.1 A model binder - Egyszerű típusok és a beépített lehetőségek 1-220
komplexebb az adatbázis sémánk, annál több ilyen részinformációs Action+View+Modell hármast kell
generálnunk. Akkor kezd majd igazán elkeserítő lenni a fejlesztés, amikor ez a modellszaporulat
átfedéseket is mutat egymással a propertyk alapján.
A másodiknál szintén hasonló gondok vannak, ráadásul még a típus konverziókat és a validációt is
manuálisan kell megoldani. Ez már 10 input mezőnél sziszifuszi munkának fog tűnni.
A harmadiknál nincs is gond látszólag. A (Try)UpdateModel ugyanis csak azokat a propertyket fogja
update-elni, amik a HTML form alapján érkeznek. A többit érintetlenül hagyja. Emiatt az adatforrásból
érkező teljes modellstruktúra átadható számára.
Mivel az első kettővel sokat nem tudunk kezdeni, nézzük most ezt a harmadikat részleteiben. Annak
ellenére, hogy ez ígérkezik a legjobb megoldásnak ezzel is vannak problémák, bár nem olyan feltűnőek
elsőre, ezért érdemes kicsit átgondolni mik adódhatnak vele. A lista a „szélsőértékeket” tartalmazza,
tehát nem biztos, hogy az MVC környezetben eltöltött első év alatt minddel fogunk találkozni. Szóval
semmi ijedelem.
- Sebesség probléma. Ha a modellünk egy igen összetett modell, mert tegyük fel az egy ORM
alapú osztály sok-sok további típusos listákkal, navigációs propertykkel, akkor minden egyes
propertyhez próbál keresni egy form elemet, név alapján. Bejárja az összetett típusú propertyk
propertyjeit, a listák propertyjeit. Ez listák, komolyabb gráfok esetén rendkívül le tudja lassítani
a bindolási folyamatot. Láttam már olyat is, hogy >2 másodpercig tartott egy ilyen művelet.
- „Over posting” vagy más néven a „Mass assignment” probléma. Ez egy hackelési módszer,
amikor a form post csomagot olyan további mezőkkel bővítve küldik el a szerver számára, amik
nem szerepelnek a gettel lekért oldalon. Példaként tegyük fel, hogy megint csak az email címet
megváltoztató formot szeretnénk elkészíteni. A TryUpdateModel megkapja a form adatai
között az email címet és be is frissíti a modell email propertyjét. De a TryUpdateModel
módosítja a modell többi propertyjét is, ha azok megérkeznek a requesttel. Azt nem nézi, hogy
milyen form küldi az adatot. Így a post requestbe bele tudunk csempészni egy UserName nevű
értéket, aminek a megváltoztatását nem engedi a feltéttelezett üzleti igény. Ha benne van a
requestben, akkor az is átírásra kerül, függetlenül attól, hogy nincs is ott a megelőző get
requesttel, az általunk kiküldött formon. Kérdés lehet, hogy honnan tudja a hacker, hogy
egyáltalán ilyet lehet csinálni? Egyrészt a webszerver elküldi a response fejlécében, hogy a
rendszerünk ASP.NET kiszolgálási környezetben fut. A „UserName” pedig a regisztrációs web
formon vélhetőleg azonos <input> névvel szerepel. De ezek csupán elképzelt lehetőségek
voltak…
- Validációs probléma. Ha a modell propertyre előírtam például azt, hogy kötelezően kitöltendő
(Required), de mégsem szerepel a beérkező input mezők között, akkor ez validációs hibát
okozhat, ha nem töltjük ki. Ráadásul, ha nincs a View-n egy ValidationSummary megjelenítő,
akkor egy hibajelzés nélküli hibát fogunk kapni. A felhasználói élmény ez lesz: „Valahogy csak
nem akarnak elmentődni az értékek”.
8.1 A model binder - Egyszerű típusok és a beépített lehetőségek 1-221
- Üzleti logikai probléma. Lehetséges, hogy a modell egyes propertyjeit csak a többi property
függvényében kell frissíteni. Például egy checkbox állapotától függhet egy másik input mező
tartalmának elfogadhatósága/szükségessége.
A problémák egy részére adható megoldások közös jellemzője, hogy közöljük a model binder-rel, hogy
melyik propertykkel kell foglalkoznia egyáltalán. Erre öt egyszerű megoldásunk is van. A megoldandó
feladat mindegyik példában az, hogy a formon levő mezők közül csak a „FullName” property adatai
frissüljenek, a többivel ne foglalkozzon a model binder. Az első hármat ebbe a kódrészletbe sűrítettem.
[HttpPost]
public ActionResult Edit6(int id)
{
var model = CategoryModel.GetCategory(id);
//1. Interface
if (this.TryUpdateModel<ICategoryFullNameUpdateModel>(model))
{
//MindenOk.
bool isValid = this.ModelState.IsValid;
}
return RedirectToAction("Index");
}
Az első megoldás - számomra a legszimpatikusabb - egy interfésszel írja elő a modellnek azt a részét,
amit frissíteni kell. Ekkor az interfész kevesebb propertyt határoz meg, mint ami a modellen van
definiálva. Most például csak ennyit:
Ennek a módszernek az előnye, hogy az interfészt más esetben is felhasználhatjuk. A hátránya, hogy
az interfész definíciókat karban kell tartani, de ez talán egyszerűbb, mint sok-sok egyedi modellt
gyártani, és ezeket mappelni például az ORM osztályához.
A második megoldásban egy property névtömbbel adom meg mely propertykkel foglalkozzon a mb. A
harmadikban pedig, fordítva, azt határozhatom meg, hogy mikkel ne foglalkozzon.
[HttpPost]
[ValidateOnlyFormFieldsAttribute]
public ActionResult Edit7(
[Bind(Include = "FullName", Exclude = "CreatedDate,WillNeverValid")] CategoryModel inputmodel)
{
var model = CategoryModel.GetCategory(inputmodel.Id);
model.FullName = inputmodel.FullName;
return RedirectToAction("Index");
}
A modell validációs problémája még mindig fennáll mind az öt esetben. Az ötödikben csak elvileg, mert
nem életszerű validálni olyat, amit úgysem tud a felhasználó megváltoztatni. Próbaképpen a
WillNeverValid propertyt használom, ami nem jelenik meg a View formon, így a model binder
TryUpdateModel metódusa ki fog rajta akadni, ha valahogy nem töltjük fel kódból.
[Required]
public string WillNeverValid { get; set; }
Az isValid lokális változó false lesz az előbbi Edit7 action esetében. Ennek elkerülésére van két egyszerű
és egy jobb megoldás. Az 1. verziós egyszerű megoldás, hogy nemes egyszerűséggel töröljük a
validációs listából a hiányosságra felhívó bejegyzést:
this.ModelState["WillNeverValid"].Errors.Clear();
bool isValid = this.ModelState.IsValid; //True lesz
A 2. verziós egyszerű megoldás, hogy kódból biztosítjuk a model hiányzó értékeit. Ez az Edit6-os
változatnál például így nézhet ki :
[HttpPost]
public ActionResult Edit6(int id)
{
var model = CategoryModel.GetCategory(id);
model.WillNeverValid = "Csak legyen valami";
//1. Interface
if (this.TryUpdateModel<ICategoryNevUpdateModel>(model))
{
//MindenOk.
bool isValid = this.ModelState.IsValid;
}
De ez csak az Edit6-nál használható jól, ahol az action paraméterlistájában nem várom a modellt,
hanem manuálisan indítom a model binder-t hogy a modellt kitöltse. Az Edit7-nél a gond, hogy a teljes
modell szerepel, mint paraméter, tehát a validáció már az előtt lezajlik, hogy az általunk írt action
kódban megvalósítható utólagos adatfeltöltés futni kezd. Ha valami úgy kezdődik, hogy „az előtt, mint
az action metódus kódja”, akkor egyből eszünkbe juthat, hogy ez az action filterek egyik jellemzője. Így
kicsivel jobb megoldást egy filter attribútummal biztosíthatunk, ami az előző Error.Clear() műveletet
generalizálja azzal, hogy törli az összes olyan validációs hibát, amihez nem tartozik input mezőnév.
8.2 A model binder - Felsorolások, listák és szótárak 1-223
var keysWithNoIncomingValue =
modelState.Keys.Where(x => !fc.Controller.ValueProvider.ContainsPrefix(x));
Használata:
[HttpPost]
[ValidateOnlyFormFieldsAttribute]
public ActionResult Edit7([Bind(Include = "FullName", Exclude = "CreatedDate")] CategoryModel
inputmodel)
{
var model = CategoryModel.GetCategory(inputmodel.Id);
model.FullName = inputmodel.FullName;
return RedirectToAction("Index");
}
Természetesen ez egy vitatható megoldás, mert pont azért vannak a modellben a validációs
attribútumok, hogy biztosítsák az adat érvényességét. Viszont bemutat egy lehetőséget arra, hogy az
aktuálisan nem használt propertyk validációját átléphessük, ha ilyen modellt használunk. Abban az
esetben, ha az action paramétere egy modell, ami történetesen egy (komplex) ORM entitás, amit a
módosított adatokkal vissza szeretnénk írni az adatbázisba, akkor valószínűleg szükségünk lesz egy
további lépésre, amiben a bejövő modell adatokat összefésüljük az adatbázisból származó entitással.
Amit ki szeretnék emelni lezárásként, hogy fontos észben tartani az implementációs és a tervezési
fázisban is, hogy az alapértelmezett bindolási mechanizmus a posttal érkező input mezők alapján
minden név alapján összeegyeztethető modell propertyt kitölt.
Az előbbi részt az „összeegyeztethető” homályos szóval zártam le. Ebben a szakaszban megnézzük,
hogy mi a helyzet az indexelhető tartalmakkal, amivel kapcsolatban az input mező és a property név
alapján történő összerendelését is részletesen megvizsgáljuk. Az eddigiekre építve egy komplexebb
példát nézünk meg. Továbbra is a BinderDemoController-ben vannak a példák megvalósítva.
Az actionben nem történik más, mint beállítunk egy depth nevű elemet a ViewData-ba és kérünk egy
komplett modellt a GetList segítségével. A depth szerepe csak annyi, hogy az egyre mélyebb szinteket
más-más megjelenítési stílussal tudjuk jelezni. Történetesen egyre sötétebb lesz a blokk háttere. Íme,
a modellgenerátor:
8.2 A model binder - Felsorolások, listák és szótárak 1-224
public static List<CategoryModel> GetList(bool recreate = false, int itemNumber = 2, int deep = 2)
{
if (recreate || datalist == null)
{
tid = 0;
datalist = CreateListInner(itemNumber, deep);
}
return datalist;
}
A GetList egy belső metódussal előállít egy listát a CategoryModel objektumokból „itemNumber”
mennyiségű elemmel. Mivel a CategoryModel-nek van egy további listája SubCategories néven azt is
feltölti egy új listával. Ezt teszi „deep” szintig rekurzívan.
return result;
}
Ezzel lesz egy olyan listánk, aminek vannak egyszerű típusai és listái az adott mélységig. A megjelenítő
View aránylag egyszerű. Egy generikus listát fogad majd az elemein végigiterálva. Egy táblázat sorait
képzi belőle a DisplayFor segítségével.
@model System.Collections.Generic.List<MvcApplication1.Models.CategoryModel>
<h2>Hierarchy</h2>
@Html.ActionLink("Lista újragenerálása","RecreateTree")
<br/>
<table style="width: 100%; background-color: #ddd;">
<tr>
<th>Id</th>
<th>Név</th>
<th>Létrehozva</th>
</tr>
@foreach (var m in Model)
{
@Html.DisplayFor(item => m);
}
</table>
@using MvcApplication1.Models
@model CategoryModel
@{ int index = 0; int depth = (int)ViewData["depth"];}
</td>
<td>
@Html.DisplayFor(model => model.CreatedDate)
</td>
</tr>
<tr>
<td colspan="3">
@if (Model.SubCategories != null)
{
<div style="padding: 8px; margin-left: 12px;@CategoryModel.GetColorOfDepth(depth)">
<table style="width: 100%;">
<tr>
<th>Id</th>
<th>Név</th>
<th>Létrehozva</th>
</tr>
@foreach (var item in Model.SubCategories)
{
@Html.DisplayFor(m => item, "CategoryModel", "SubCategories[" + index++ + "]",
new { depth = depth + 1 })
}
</table>
</div>
}
</td>
</tr>
Látható, hogy a listák egymásba vannak ágyazva, amit az egyre sötétedő blokkok is jeleznek. A
Kategórianevek egyben linkek is és a szerkesztő oldalra visznek. A szerkesztő oldal action metódusa
és megjelenése:
Elnézést, ha ez egy kicsit hosszú volt. A számunkra most hasznos eredmény a generált HTML kódban
látható. A markupokból ismételten kivettem a nem fontos részeket. Nézzük az első táblasort, aminek
az Id=0 az azonosítója:
<tr>
<td>
0
<input id="Id" name="Id" type="hidden" value="0" />
</td>
<td>
<input id="FullName" name="FullName" type="text" value="Kategória 2-0" />
</td>
<td>
<input id="CreatedDate" name="CreatedDate" type="datetime" value="2013.03.20. 14:55:31" />
</td>
</tr>
Az Id nevű modell propertyt egy name=”Id” attribútumú input mező képviseli. A FullName propertyhez
egy name=”FullName” tartozik, stb. Ha csak ennyit tartalmazna a View és ezt mentenénk el, akkor a
model binder képes lenne beállítani ezek alapján a modellt. A beágyazott osztály és struktúratípusok
tulajdonságait PropertyNév.PropertyNév formában nevezi el az MVC. Például, ha elhelyezzük a
következő sort az EditorTamplates/CategoryModel.cshtml fájlban.
@Html.EditorFor(model=>model.JoinedCategory)
Erről ennyit érdemes tudni, de mi a helyzet, ha a property történetesen egy lista, szótár vagy tömb?
Ezek után következzenek az Alkategoria listaelemei:
<tr>
<td colspan="3">
<div>
<table style="width: 100%;">
<tr>
<th>Id</th>
<th>Név</th>
8.2 A model binder - Felsorolások, listák és szótárak 1-227
<th>Létrehozva</th>
</tr>
<tr>
<td>1</td>
<td>
<input id="SubCategories_0__FullName" name="SubCategories[0].FullName" type="text"
value="Kategória 1-1" />
</td>
<td>
<input id="SubCategories_0__CreatedDate" name="SubCategories[0].CreatedDate"
type="datetime" value="2012.12.06. 14:55:31" />
</td>
</tr>
. . .
Ez csak egy részlet, a tábla még folytatódik.
name="SubCategories[0].FullName"
name="SubCategories[0].SubCategories[0].FullName"
name="SubCategories[0].SubCategories[1].FullName"
name="SubCategories[1].FullName"
name="SubCategories[1].SubCategories[0].FullName"
name="SubCategories[1].SubCategories[1].FullName"
Ezt a model binder teljesen jól megérti, ha posttal elküldjük egy actionnek. Minden egyes
SubCategories listaelemhez példányosít egy-egy CategoryModel osztályt, és ezeknek a modell
objektumoknak is feltölti a listáit.
[HttpPost]
[ActionName("EditTree")]
public ActionResult EditTreePost(int id)
{
var model = CategoryModel.GetCategory(id);
this.TryUpdateModel(model);
return RedirectToAction("ListTree");
}
Az ActionName attribútum segítségével más metódusnevet is használhattam, hogy ne ütközzön a get
requestet kiszolgáló EditTree actionnel.
Némi tapasztalati információt ad a model binder sebességéről, ha a GetList metódus paramétereivel
elkezdünk játszani. Az alábbi metódushívásnál beállítottam a soronkénti alkategóriák számát és a
mélységet is 5-re.
public static List<CategoryModel> GetList(bool recreate = false, int itemNumber = 5, int deep = 5)
Az eredmény 19529 sor lett. Ennek az első elemét szerkesztve 3905 sornyi szerkesztő mezőt kaptam.
Ez 2 x 3905 = 7810 látható és további 3905 hidden mezőt jelent (=11715). A szerkesztés után a form
mentése során, a model binder ~10 másodperc alatt alkotta meg újra az egész hierarchikus struktúrát
az input mezőkből. Ez persze nem etalon, csak tájékoztató jellegű adat.
8.2 A model binder - Felsorolások, listák és szótárak 1-228
A példákban az input mezők elnevezései - hogy lássuk a névkonvenciókat is – félig manuálisan lettek
meghatározva. Emlékeztetőnek:
Pedig erre nincs szükség, ha az EditorFor-t használjuk egy felsoroláson indexelve, mert az MVC
felismeri, hogy ilyen indexelt listaelemmel van dolga és a helyes elnevezést is biztosítja. Az alábbi
azonos eredményt szolgáltat:
A model binder megérti az Array és a Dictionary alapú listákat is. Ez utóbbira is készült demó
DictionaryTree, EditDictionaryTree és EditDictionaryTreePost action metódusokkal feldolgozva. Csak a
lényeget kiemelve a dictionary indexe az „Di”+Id -ből tevődik össze. A metódusokban az Id, mint
elemazonosító nem kap szerepet, hiszen a dictionary indexé lesz ez a szerep.
var id = tid++;
var cm = new CategoryDictionaryModel
{
Id = id,
FullName = string.Format("Kategória {0}-{1}", deep, id),
CreatedDate = DateTime.Now.AddDays(deep * Rand.Next(20, 100) - 200),
};
if (deep != 0)
cm.SubCategories = CreateListInner(itemNumber, deep - 1);
result.Add("Di" + id, cm);
Az editor template-ben a lényeges elemek ki lettek vastagítva: a key nevű hidden mező, és a
SubCategories elemeinek előállítása. Az alkategóriák indexe a dictionary aktuális kulcsa.
<td>1
<input id="SubCategories_Di1__Id" name="SubCategories[Di1].Id" type="hidden" value="1" />
<input id="SubCategories_Di1__key" name="SubCategories[Di1].key" type="hidden" value="Di1" />
</td>
<td >
<input id="SubCategories_Di1__FullName" name="SubCategories[Di1].FullName" type="text"
value="Kategória 2-1" />
</td>
<td >
<input id="SubCategories_Di1__CreatedDate" name="SubCategories[Di1].CreatedDate" type="datetime"
value="2013.01.18. 22:57:35" />
</td>
8.3 A model binder - Bonyolult modellek problémái 1-230
Egy komplex modellben nagyon valószínű, hogy lesznek olyan propertyk, amiknek kezdeti értéket kell
adni és olyanok is, amik értékeit valamilyen listák alapján lehet kiválasztani. Ez utóbbiak általában
valamilyen combobox vagy kiválasztható listaelemek alapján kapják meg az értékeiket. Csak
példaképpen egy magyarországi utcanévlistára gondoljunk, vagy egy komolyabb termékkategória
listára. Tehát szükséges lesz a választható listaelemek feltöltése. Ezeket a listákat beletehetjük a
modellbe, mivel úgy is a View fogja felépíteni a vezérlőket a listatartalmak alapján. A fő kérdés, hogy
mikor és hogyan inicializáljuk a propertyket és hogyan töltsük fel a modellünk listáit? A model binder
viselkedése miatt semmiképpen sem a konstruktorban! Sőt a modell konstruktorában levő kódot
annyira minimalizáljuk, hogy lehetőleg semmilyen kódot se rakjunk bele. Mivel a default konstruktor
le fog futni a binder által, így az abban megvalósított kód is le fog futni, holott nem valószínű, hogy
inicializálásnak hasznát vesszük abban a helyzetben, amikor a request értelmezése és egy modell adat
összeállítása a cél. Arra az esetre, amikor a modellünket a View számára töltjük fel, hozzunk létre egy
külön adatfeltöltéssel és inicializálással foglalkozó metódust, vagy egy alternatív, paraméteres
konstruktort, amit a model binder nem használ.
A következő általános szituáció, hogy a modellünk tartalmazni szokott számított értékű propertyket is.
Ezek azok, amiknek csak gettere van és bennük számításokat és string összefűzéseket tartalmazó kódot
szoktunk implementálni. Az ilyen kódok támaszkodhatnak más kódokra, további számított értékekre
is. A probléma ott kezdődik, ha ezeknek a számított mezőknek megálmodott propertyknek setter
ágaikat is implementáljuk. Anélkül, hogy most elkalandoznék a tervezési hibák és tiszta kódolás 41
világába, a lényeg az, hogy az összes propertyt, aminek van settere is, a model binder kezelésbe fogja
venni és megpróbál értéket adni neki. Azonban, ha a setterben, olyan kód van, ami megváltoztat más
értékeket is a modellben, akkor azoknak a kódja is be fog indulni mikor a model binder értéket ad az
alap propertynek.
Egy másik jelenségről is érdemes szót ejteni, ezt valahogy úgy nevezhetnénk, hogy modellcsokor
probléma. Maga a kifejezés is legalább olyan furcsán hangzik, mint a megvalósítás, amit takar. Tegyük
fel, hogy eredetileg azt terveztük, még a modell szűzleány korában, hogy a lehető legkevesebb
propertyt használunk. Aztán a felület bonyolódik és az input mezők száma elkezd növekedni, ahogy az
alkalmazás is bővül. Megjelennek az alap View-hoz és modellhez nem teljesen kapcsolódó adatok is.
(Pl. a partial View-k saját almodellt igényelhetnek). Vagy már dönthetünk az elején is (rosszul) úgy,
hogy mindent egybe alapon létrehozunk egy superglobaluniversal adatokkal és funkciókkal bíró
modellt, amit aztán az alkalmazásunk legtöbb View-ja számára használható lesz. Ez lesz a
modellcsomagunk vagy csokorosztályunk, ami nem csinál mást, mint más objektumokat tárol, amik
külön-külön önmaguk is egy modellek. Ez egyébként egy kényelmes modelltervezési megközelítés,
mert nem fektet energiát az OOP-s szabályok betartásába. Ahogy azonban ez lenni szokott, ennek az
41
Ajánlott irodalom: Robert C. Martin: Tiszta kód.
8.3 A model binder - Bonyolult modellek problémái 1-231
árát egyszer meg kell fizetni, és ha máshol nem is a model binder-nél valószínűleg meg fogjuk fizetni
„lassúság” valutában. A binder ugyanis be fogja járni a teljes modellstruktúránkat, nem kevés időt
vesztegetve a modell újraépítésével és az almodellek példányosításával. Akkor azonban, ha a
modellünk egyben egy ORM osztály is, nehezebben tudjuk azt a helyzetet kezelni, amikor az ORM egy
komolyabb, normalizált adatbázis séma szerint épült fel.
Összeadva ezt a három fő modell felépítéssel kapcsolatos problémakört, az ajánlott elv a binder
működése miatt, hogy a modellben kerüljük azokat a helyzeteket, amelyek a modell belső változását
vagy az automatikus inicializálását indikálják és használjunk minél egyszerűbb felépítésű modellt. De
mi a helyzet, ha mégsem tudjuk kikerülni a fenti helyzeteket, mert már évek óta készen van a modell,
és minden jól működik? Majd arra a döntésre jutunk, hogy nem tervezzük át hirtelenjében, mert
kártyavárként omlana össze a rendszer. Erre is tartogat megoldást az MVC, mivel a model binder
rendszerébe is be tudunk avatkozni és modellre szabott bindolási mechanizmust tudunk
implementálni.
8.4 A model binder - Mélyen belül 1-232
Következzen az a rész, amiben megnézzük, hogy pontosabban mi és miért történik az MVC belső
világában a request feldolgozásakor. De mielőtt belevágnánk: ez egy kicsit bonyolultabb rész. Emiatt,
ha az előző fejezet fárasztó volt, ez az egész fejezet nyugodtan átugorható. Első nekifutásra a
következők ismerete nélkül is komplett MVC alkalmazást tudunk felépíteni. Főleg, ha egyszerű
modellekből építkezünk. Viszont, ha mégis bevállalható, akkor ki fog derülni néhány olyan részlet is,
hogy például miért nem tanácsos a modellen Action nevű propertyt definiálni.
Amikor kezünkbe vesszük az irányítást a model binder felett, rájöhetünk hogy ez a komponens legalább
olyan fontos része az MVC működésének, mint maga a modell amivel dolgozik. Innentől nevezhetjük,
akár MBVC-nek is a technológiát. Eddig model binder-ről beszéltem egyes számban. Ami inkább egy
általánosítás vagy gyűjtőnév arra a működésre, ami a string alapú név-érték párokat típusos vagy akár
még hierarchikus adattá is tudja alakítani. A valósághoz közelebb áll, hogy van a bindolás számára
interface definíció, ami csak egyetlen feladatot ír elő:
HttpPostedFileBaseModelBinder
ByteArrayModelBinder
LinqBinaryModelBinder
CancellationTokenModelBinder
FormCollectionModelBinder
DefaultModelBinder
Amikor egy modell feltöltése szükségessé válik, (pl. TryUpdateModel hívásával) az MVC megpróbálja
megkeresni a modell típusához megfelelő bindert. Az alább felsorolt lépésekkel próbálkozik és az első
sikeres találat szerinti megvalósítást fogja használatba venni.
1. Megnézi, hogy a bejegyzett model binder providerek között modell típus alapján talál-e
megfelelőt. Alapértelmezetten nincs bejegyezve egy ilyen provider sem az MVC4-ben.
2. Megnézi az előbb felsorolt IModelBinder megvalósítások első négy eleme közül, hogy
valamelyik használható-e, pontos modell típusegyezés alapján.
3. Megvizsgálja, hogy létezik-e a modellen CustomModelBinderAttribute leszármazott. Ha igen,
akkor azt használja. Ide tartozik a FormCollection modell típus, amire a
8.4 A model binder - Mélyen belül 1-233
A listából látszik, hogy négy bővítési pont is van, ha egy saját model binder-t szeretnénk használni.
Visszafelé lépdelve a lehetőségeken, a negyedik, hogy módosítjuk/felülbíráljuk a default model binder-
t. Azonban a leszármaztatásával vagy újraírásával csak speciális esetben érdemes próbálkozni. Hagyjuk
meg inkább az általános esetekre. A sorban visszafelé a 3. eset, hogy a modellünket kidekoráljuk a
CustomModelBinder attribútum leszármazottjával, amiben felülbírálva a GetBinder metódusát, egyedi
modell specifikus bindert tudunk példányosítani. A 2. lehetőség által sugallt megoldás, hogy a gyári
binderek listáját bővítjük. Ez sem a legjobb, ha összehasonlítjuk azzal, amit az 1. lehetőség ad, hogy
tudjuk „bővíteni” az üres provider listát. A kettő között van egy apró különbség. A 2. lehetőségben egy
dicionary-ba kell bejegyeznünk a saját binderünket. A bejegyzés indexe az a modell típus, amihez
használnánk a saját bindert. Ennek a hátránya, hogy csak egzakt típust tudunk megadni, azaz, ha több
modellhez is használni szeretnénk az egyedi binderünket, akkor mindhez kell egy bejegyzést csinálni.
Nincs mód arra, hogy modell ősosztályt vagy interfészt regisztráljunk. Kevés modellnél és egyedi
binddernél talán még használható is. Az interneten fellelhető példák/demók zöme is ezt a megoldást
mutatja be (nagyon érdemes megnézni a cikkek készítésének idejét). Az 1. bővítési pontot támogató
lépés abban tér el a 2. lépéstől, hogy az binder kiválasztása „körkérdéssel” dől el. A providerek végig
lesznek kérdezve a modell típussal, hogy van-e hozzá binderük. Ha van, szolgáltatják és akkor az lesz
az aktív binder, ha nem akkor null-al visszatérve továbbpasszolják a listában a következő providerhez
a kérdést.
Kezdjük el a munkát azzal a céllal, hogy akarunk egy saját model bindert, mert a default mégsem
annyira jó. Íme, egy primitív binder, ami a rövidség kedvéért nem foglalkozik mással csak a FullName
és a CreatedDate tulajdonságok kitöltésével, mellőzve a validációt is. (2. próbálkozási szint a mb.
keresési listájában)
id = Convert.ToInt32(request.Form.Get("Id"));
fullName = request.Form.Get("FullName");
createdDate = Convert.ToDateTime(request.Form.Get("CreatedDate"));
id = (int)bindingContext.ValueProvider.GetValue("Id").ConvertTo(typeof(int));
fullName = bindingContext.ValueProvider.GetValue("FullName").AttemptedValue;
createdDate = (DateTime)bindingContext.ValueProvider.GetValue("CreatedDate")
.ConvertTo(typeof(DateTime));
8.4 A model binder - Mélyen belül 1-234
Ezek után közölni kell valahogy az MVC-vel, hogy használja az új model binder-t. Elsőnek a „nem
annyira jó” megoldással kezdjük, a Binders listának a bővítési módszerével. Mivel egy statikus
dictionaryt kell bővíteni ezért a legjobb hely ennek kivitelezésére a global.asax fájl. Amiben nem kell
mást tenni, mit a Binders dictionarybe belerakni a saját binderünket. A szótár indexe az a modell típus,
amihez a binder tartozik. Jelen esetben a CategoryModel.
Tehát ha ilyen mintát látunk valahol, akkor emlékezzünk, hogy ez ma már, nem a legjobban ajánlott
megközelítés.
Helyette bővítsük a ModelBinderProviders gyűjteményt szintén a global.asax-ban. (1. próbálkozási
szint a mb. listájában)
Ennek a providernek a haszna akkor jelentkezik, amikor sok modellel rendelkezünk, amik egy
leszármazási lánc vagy interfész megvalósításai. A GetBinder-ben speciálisan el tudjuk dönteni, hogy a
provider által szolgáltatható binder illeszkedik-e az adott modell típushoz. Az előbbi példában a
CategoryModelBinderProvider a CategoryModelBinder-t fogja felkínálni a CategoryModel-hez és
ennek a leszármazottaihoz is az IsAssignableFrom miatt. (Egy typeof(CategoryModel) == modelType
csak pontos típusegyezést vizsgálna, leszármazottakat nem)
Nem vagyunk korlátozva abban , hogy a saját model binder-ünket csak éppen annyira implementáljuk,
amennyire lefedi a speciális igényünket, a bindolás hagyományos részét a default model binderrel
végeztethetjük el. Ehhez elég egy DefaultModelBinder-t példányosítani és meghívni a BindModel
egyébként virtuális metódusát, a ControllerContext és ModelBindingContext paraméterekkel, amit a
saját binderünk is megkapott.
Azt már nagyjából tudjuk, hogyan lehet bővíteni a rendelkezésre álló binderek listáját, de a „minek?”
kérdést még fel sem tettem. Tehát milyen esetben lehet szükség erre? Csak néhány tipp:
Ha a modellünk nagyon összetett. Több felsorolást és típust hosztol. Ilyen esetben javítani
lehet a feldolgozási sebességen.
Ha a modellünk erősen interfész alapú vagy interfész típusú propertyjei vannak. A default
bindernek ötlete sem lesz, hogy honnan szedje a konkrét interfész megvalósításokat. Hasonló
a helyzet az absztrakt típusokkal.
Ha a bejövő request adat eltér a megszokottól. Például lehetséges a cookie adatokat is
értelmezni, amire nincs beépített binder megvalósítás. Hasonló ok lehet, ha a javascriptből
jövő JSON adatokat speciálisan szeretnénk értelmezni.
Ha a modellt és annak osztály alapú propertyjeit nincs értelme példányosítani, mert ORM
kontextus függőek és/vagy a felparaméterezett példányokat egy factory osztály állítja elő.
Hasonló a helyzet, amikor a modellnek (szolgáltatás) függőségei vannak. Erre megoldást
nyújthat a Dependency Injection elv.
Amikor a post request a megelőző get requestben hidden mezőkben tárolt kódolt adatokat
tartalmaz, amiket vissza kell alakítani. Ezzel tudunk az ASP.NET ViewState-hez hasonló
működést produkálni, vagy egyéb hidden csomagokat elhelyezni a HTML kódban, amik
körutazáson vesznek részt.
Vegyük elő egy kicsit a ValueProvider-t, ami szintén érdemel néhány szót, ha másért nem hát azért,
mert ez is egy bővítési pont az MVC framework-ben. A feladata nem más, minthogy a requestben
szereplő adatokból név alapján szolgáltasson egy értéket. A „név” lehet az input mező neve, URL
paraméter (query string) neve és még továbbiak is. Ebből látszik, hogy a ValueProvider egyetlen
értékkel foglalkozik, amiből a model binder egy property vagy egy action paramétert fog feltölteni.
ChildActionValueProviderFactory
A Html.Action metódus hívásakor esetlegesen átadott route paraméterek név érték párjai.
Child action esetében.
FormValueProviderFactory
A HTML form input mezői
JsonValueProviderFactory
Az Ajax JSON formátumú post adatai
RouteDataValueProviderFactory
Az aktuális route bejegyzések (controller,action,id, stb)
8.4 A model binder - Mélyen belül 1-236
QueryStringValueProviderFactory
Az URL paraméterek név-érték párjai
HttpFileCollectionValueProviderFactory
Az aktuálisan feltöltött fájl tartalma.
ValueProviderFactoryCollection.GetValueProvider()
metódusával szerezhető meg . Az egészben van azonban egy csavar, mégpedig az, hogy ez a metódus
egy ValueProviderCollection-t ad vissza, ami maga is egy IValueProvider, és végezetül ennek az
collection alapú osztálynak a GetValue metódusában dől el, hogy melyik valódi ValueProvider fogja
szolgáltatni a ValueProviderResult-ot. Ez egy kicsit bonyolultnak tűnhet, de mindjárt egy példán
keresztül remélhetőleg könnyebben átlátható lesz. Valójában elég ha tudjuk, hogy hol szerezhető meg
az elosztó szerepét betöltő ValueProviderCollection.
A kontrollerben a Controller.ValueProvider
A model binder környezetben a ModelBindingContext.ValueProvider (ez a
Controller.ValueProviderre egy referencia)
Az ismerkedést folytassuk a normál működés néhány sajátosságával három példán keresztül, ami sok
apró részletről rántja le a leplet.
Mind a két helyi változóba a route aktuális értéke kerül. Ezért említettem a routinggal
foglalkozó fejezetben, hogy az egyértelműség miatt kerülendő az „action” és a „controller”
nevű propertyk használata, mert ezzel megtudjuk magunkat tréfálni. Például, ha történetesen
a modellünkön van „action” nevű tulajdonság és a hozzá tartozó <input> mező, akkor az action
változóba annak az értéke kerül és nem a ”action” route név, azaz az aktuális action neve.
Ennek az oka, hogy a FormValueProvider előrébb van a provider felsorolási sorban.
{
@Html.Partial("EditPartial", Model)
}
3. Nem sokkal ezelőtt néztük az over-posting problémát, amikor input mezőket hazudva tudtuk
módosítani a modellt. A providereket elnézve már bizsereg a kezem alatt a billentyűzet, hogy
továbbvigyem ezt a témát. Módosítsuk az előző form példát, úgy hogy beteszünk egy további
route paramétert, de input mezőt nem:
A fenti problémákon kívül még továbbiakba is belebotolhatunk a fejlesztés során, mivel a bejövő
request adatok változatosságában a név-érték párok „név” indexe átfedésbe kerülhet egymással. Mit
lehet tenni, hogy ezt ki tudjuk védeni? Meg kell határozni a model bindernek, hogy milyen
ValueProvider-ekkel dolgozzon és, azt is, hogy milyen sorrendbe tegye azt. Erre pedig megvan a
módszer a TryUpdateModel metódus túlterhelt változataiban. A demó action a
BinderDemoController. FixValueProvider actionben van megvalósítva a példakódban. A View
lényegében megegyezik az előbb látottal:
Az egyik a route paraméterben, amiből képződni fog egy „Id” bejegyzés, amit majd a
RouteDataValueProvider, és egy „WillNeverValid”, amit a QueryStringValueProvider fog tudni
értelmezni.
A másik hely a route bejegyzés nevekkel azonos input mezők, amit a FormValueProvider tud
kezelni.
8.4 A model binder - Mélyen belül 1-238
Az előző 3. példánál láttuk, hogyha használnánk a normál ValueProvider-t, akkor a form input
mezőkben levő adatok előnyt élveznének a bindolás során és az URL paraméterek elvesznének. A
következő action metódusban meghatározzuk, hogy melyik IValueProvider szolgáltasson adatokat:
[HttpPost]
[ActionName("FixValueProvider")]
public ActionResult FixValueProviderPost()
{
//Value provider csak az URL paraméterrel dolgozik
var querystringValues = new QueryStringValueProvider(this.ControllerContext);
var routeValues = new RouteDataValueProvider(this.ControllerContext);
return RedirectToAction("FixValueProvider");
}
Példányosításra kerül egy QueryString-es és egy RouteData ValueProvider. Az első három GetValue
próbálkozás a QueryStringValueProvider-el rendre null-t fog adni, mert nem tud a kért kulcsokról. Az
int típusú Id-t a routeValues (a RouteDataValueProvider) helyesen szolgáltatja. És szintén jól fog
működni a QueryStringValueProvider az „WillNeverValid” kulccsal. A formon levő input mezők értékei
nem kerülnek elő egyik esetben sem. Látva, hogy manuálisan jól működnek a ValueProvider-ek, a
TryUpdateModel metódusnak is átadhatjuk a meghatározott QueryStringValueProvider példányt
(querystringValues). Ennek eredménye pedig az lesz, hogy a CategoryModel.WillNeverValid
propertyje, helyesen a „Ez query string lesz” szöveget fogja kapni.
Ez utóbbiról egy kicsit bővebben beszéljünk, mert hasznos lehet a részleges modell bindoláshoz.
Tegyük fel, hogy a modellünk egy összetett osztály, ami további részmodelleket hordoz. A modellünkön
szerepel egy „Submodel” ImportantModel típusú property a saját tulajdonságaival. A renderelés után
egy ilyesmi HTML részletet kaphatunk egy formba zárva:
A post során beérkezik a request, de a túlburjánzott modellünkön olyan további modellek vannak, amik
nem képviselnek hasznos bemeneti adatokat az adott action szempontjából. A model binder
megpróbálja a szükségtelen propertyket is feltölteni. Megvan a lehetőségünk, hogy a TryUpdateModel
metódusban a prefix paramétert megadva a binder és a ValueProvider csak a Submodel-el
foglalkozzon. Ebben a példában még azt is kikötöttem, hogy a bindolás csak a form input mezői alapján
történjen:
Talán ez a fejezet merült el legmélyebben az MVC belső világában. Nem gondolom, hogy
haszontalanul. Mindenesetre pihenésképpen egy sokkal attraktívabb téma következik.
9.1 A biztonság és az értelmes adatok - A rendszer biztonsága 1-240
Ahogy a bejövő adat megérkezik a gépünk hálózati kártyájára, a mi felelőségünk, hogy legalább tudjuk
hol lehet rés a pajzson, milyen eszközök állnak rendelkezésre ezek betömésére. A szerver operációs
rendszere, a rajta futó webszerver, ezek helyes konfigurálása ugyan a rendszerüzemeltető feladata
(lenne), fontos tudni, hogy az adatlopások, behatolási rések egy jelentős részéért, az ezeken futó
alkalmazás a felelős, és csak kisebb részben a szerver. Az ok pedig nagyon egyszerűen az alkalmazásunk
vagy az alkalmazott módszerek teszteletlenségére illetve a támadó-védekező módszerek ismeretének
hiányára mutat. A jó hír, hogy az MVC verzióról-verzióra egyre több előre kész megoldást ad a
kezünkbe. Példaként egy lista, hogy hány olyan pont különíthető el egy átlagos MVC site-on, ami
biztonsági problémát rejthet:
Hiba az ASP.NET alaprendszerben, ami kihathat az MVC keretrendszerre is. Esetleg az MVC
keretrendszer hibája.
Hibás, átgondolatlan route-olás. Jogosulatlan action elérés.
Ellenőrizetlen action metódus paraméterek. HTML form input mezőknél és JSON
adatkapcsolatnál is!
Az action metódusba érkező nyers adat (modell objektum) továbbpasszolása a View számára,
majd vissza a kliensbe. Pl.: javascript injektálás. Nyers HTML renderelés egy megelőző
felhasználói input alapján. Pl.: Felhasználó által szerkeszthető HTML résztartalom mondjuk egy
CMS rendszernél.
Child actionök, partial View-k elérése URL-ből.
A POST adatok fenntartás nélküli elfogadása. Vagy a kliens oldalon HTML hidden mezőben
tárolt titkosítás nélküli adatok, amelyeket a POST feldolgozásában szintén „készpénznek” vesz
az action.
Cookie és session adatokkal és azonosítókkal kapcsolatos problémák. Még élő session
felhasználása/életben tartása másik számítógépről.
9.2 A biztonság és az értelmes adatok - A frontvonal 1-241
Biztonsági hibával rendelkező javascript könyvtárak (régi jQuery, stb). Nem ajánlott javascript
formulák meggondolatlan használata (eval(...) )
Hibás site vagy almappa web.config beállítások. Pl.: Úgy felejtett debug üzemmód.
Kezeletlen kivételek, amelyek a felhasználót a rendszerünk belső állapotáról tájékoztatják.
Laza hitelesítő adatok elfogadása. Gyenge, rövid, szótári szavas, 10 éves jelszó.
Rosszul meghatározott szerepkörök és ezekhez adott jogosítványok.
A lista nem a fontossági sorrend szerint készült és ízlés szerint bővíthetjük szakmai tapasztalatunk
alapján. Ha ehhez hozzávesszük a webszerver beállítási hibalehetőségeket, a könyvtár jogosultságokat,
az operációs rendszer támadási felületeit, akkor egész ijesztő méretű listát kapunk.
Nézzük meg azokat a helyzeteket, amikor a felhasználó adatokat visz be a rendszerünkbe kénye-
kedve, jó- vagy rosszindulata szerint. Igyekszem a bemutatást az MVC belső feldolgozási
folyamatának a sorrendjében megtenni.
9.2. A frontvonal
Mielőtt nekiesnénk a nagybetűs validációnak érdemes a beérkező requestet alaposan szemügyre venni
és kicsit boncolgatni. Nem csak arról van szó, hogy érkezik egy dátum, vagy egy szöveges mező
tartalma. Az ilyen kézzelfogható adatok csak részletei az egész adatkontextusnak, amiben érkeznek.
Nem mindegy, hogy egy karaktersorozatot, ami lehet szenzitív adat is, milyen környezetben kapjuk
meg. Hitelesített-e a felhasználó, titkosított-e a csatorna, megfelelő-e a protokoll, mi volt a megelőző
request, stb. Az adatkontextust és a hordozott adatot külön is érdemes vizsgálni.
Az MVC framework-höz beérkező requestet első lépésben az action filterek tudják kezelésbe venni.
Mielőtt az adatokkal foglalkozni szeretnénk még döntéseket hozhatunk, hogy a request típusa és
formátuma megfelel-e az elvártnak. Az AjaxPostAttribute-os példában (7.6 JSON adatok küldése), már
foglalkoztunk ilyennel, amikor biztosak akartunk lenni, hogy a böngészőtől AJAX + post request érkezik-
e. Ez a kapcsolat érvényességét ellenőrizte, ami a csomag lényegi tartalmától függetlenül vizsgálható.
Szintén láttunk már rövid bevezetőt néhány további, beépített attribútumra. Ott van a
RequireHttpsAttribute, amivel előírható, hogy az action vagy a kontroller összes actionje csak HTTPS
kapcsolattal legyen elérhető. Ezen kívül használtuk a ChildActionOnly attribútumot is, ami kizárja, hogy
az actionünket böngészőből el tudjuk érni. Az ilyen actiont kizárólag a View-ban levő kód Html.Action
helper metódussal lehet felhasználni. Az ilyen attribútumok az IAuthorizationFilter-t valósítják meg és
az a sajátosságuk, hogyha úgy ítélik meg, hogy a bejövő adatcsomag egésze vagy a kapcsolat nem
megfelelő, akkor egy exceptiont váltanak ki. Az MVC az action meghívása előtt először begyűjti a
globális és az adott actionön és a kontroller osztályon levő filter attribútumok közül azokat, amelyek
9.2 A biztonság és az értelmes adatok - A frontvonal 1-242
[BrowserOnly("Chrome")]
public ActionResult CsakChrome()
{
return Content("Hello Chrome!");
}
Az IAuthorizationFilter-ek kiértékelése minden action filter előtt megtörténik MVC 4 esetén. MVC5-
ben még megelőzi az IAuthenticationFilter ( ) kiértékelése. Ez az első MVC-s védelmi vonal
a hamis requestekkel szemben. A beérkező adatok feldolgozásának ennél a pontjánál még egy fontos
lehetőségünk van, amit az MVC szolgáltat részben egy ilyen IAuthorizationFilter megvalósításával. Azt
az esetet vizsgáljuk most meg, amikor HTML űrlapok szolgáltatják a forrást és ezeket az űrlapokat
megelőzőleg a get requestben a mi alkalmazásunk küldte el a böngészőnek. Legalábbis szeretnénk ezt
hinni. Az űrlap mezőit visszaküldő post requestről honnan tudjuk, hogy előzőleg tényleg mi küldtük el
kitöltésre? És nem-e arról van szó, hogy egy másik gépen futó program próbálkozik éppen betörni a
rendszerünkbe vagy teleszemetelni mindent, szimulálva azt mintha egy ember ülne a böngészője
előtt? Ehhez valahogy „alá kell írni” az űrlapot. Az ellenőrzés nagyon egyszerű, ha nincs ott a beérkező
adatok között az aláírásunk, akkor az űrlap egésze nem érvényes, sőt valószínűleg rossz arcok
„próbálkoztak”. Ezzel ki tudjuk védeni az un. Cross-Site Request Forgery 42 (CSRF vagy XSRF) támadási
formát.
Ahhoz, hogy használni tudjuk a beépített CSRF védelmet, két dolgot kell csak tenni. A formba el kell
helyezni a speciális Html helpert:
[HttpPost]
[ValidateAntiForgeryToken]
42
http://hu.wikipedia.org/wiki/Cross-site_request_forgery
9.2 A biztonság és az értelmes adatok - A frontvonal 1-243
A Html.AntiForgeryToken() helper hidden input mezőt készít egy erős kóddal. Valami ilyet:
Emellett elhelyez egy session cookie-t a response-ban, amit a böngésző eltárol. A form beküldésekor
pedig elvárja a ValidateAntiForgery attribútumban levő ellenőrző kód, hogy a requestben ott legyen
mind a kettő, és passzoljanak is egymáshoz:
A hidden mezőben levő token value tartalma minden egyes oldallekéréskor változik. A cookie-ban levő
kód a böngészési session lejártáig megmarad. A kettő alapján tudja érvényesíteni a bejövő requestet.
Természetesen, ha egy elavult tokennel próbálkozunk, akkor kivétel keletkezik és hibaüzenetet
kapunk. A hidden mezőben kapott token addig érvényes, amíg a cookie token nem változik meg. Ez azt
jelenti, hogy egy generált HTML oldalra, több formban több Html.AntiForgeryToken()-t is
használhatunk. Mind hiteles lesz, még akkor is ha a value attribútumba szemmel láthatóan más és más
security token kerül. Akkor is működni fog, ha több böngészőfülben nyitjuk meg ugyanazt az oldalt. Ez
egy kompromisszum, mivel így a post request ugyan még visszajátszható (pl. Fiddlerrel), ami nem
előnyös biztonsági szempontból, viszont nem kell eltárolni az előzőleg kibocsájtott tokeneket
valamilyen kontextus (sorozatszám, timestamp) szerint. A támadónak a hidden mezőben levő kód
mellett szüksége lesz a cookie-ban tárolt kódra is. Sajnos ezek a post requestben egyszerre
szerepelnek, mint az előbbi képen is látszik. Így a hálózati kapcsolatba ékelt figyelővel lehallgatható és
megszerezhető.
System.Web.Helpers.AntiForgeryConfig.RequireSsl = true;
Meg lehet adni a cookie nevét, hogy ne legyen annyira árulkodó, hogy mi a célja:
System.Web.Helpers.AntiForgeryConfig.CookieName = "_lastproduct";
System.Web.Helpers.AntiForgeryConfig.AdditionalDataProvider =
new MyAntiForgeryAdditionalDataProvider();
9.2 A biztonság és az értelmes adatok - A frontvonal 1-244
Ezzel megsóztuk a tokent. A neve is azt mondja, hogy „additional data provider”, ezért az
alapértelmezett token validációt nem tudjuk felülbírálni, azt előbb ellenőrizni fogja, és ha az jó, csak
utána kerül a ValidateAdditionalData metódus meghívásra. A soron következő szigorú variáció csak az
utoljára kibocsájtott tokent fogadja el. Így sem többlapos/többablakos böngészés, sem több
Html.AntiForgeryToken() nem lehet egy oldalon. Csak az utoljára kibocsájtott lesz érvényes.
Ez az AntiForgery rendszer mindaddig elég kényelmesen használható, amíg HTML formokat küldünk a
szervernek. Kicsit ügyeskedni kell, ha JSON adatot szeretnénk küldeni és fogadni, mert abba valahogy
bele kell csempészni az egyébként a hidden input mezőben levő tokent. Ennek bemutatására egy cikket
ajánlok: http://reiteristvan.wordpress.com/2013/04/26/xsrf-es-ajax-asp-net-mvc-4-alatt/
9.3 A biztonság és az értelmes adatok - Felhasználó hitelesítés 1-245
Még mindig ott tartunk, hogy az actionhöz nem érkezhet el a hívás, ha a request kontextus alapján
nincsenek meg a feltételek. Jelen esetben az, hogy a felhasználónak van-e joga az actiont elérni. Két
felhasználói csoport létezik, akik hitelesítették magukat valamilyen közös titok alapján, és akik nem. Ez
utóbbiak a névtelen vagy anonymous felhasználók. A közös titok szempontjából lehet hitelesítési
módokat megalkotni, attól függően, hogy a bejelentkezéshez szükséges titkos kód hol van tárolva és
milyen módon éri el a webalkalmazás a hitelesítési folyamat során. Ezeknek a hitelesítési módoknak a
némelyike az ASP.NET alaprendszer részei és a webszerverrel szorosan integrálódva oldják meg a
feladatot. Néhány fontosabb hitelesítési módot soroltam fel, de mivel az idők és az igények változnak,
a lista nem teljes, sőt a keretrendszerek rugalmassága miatt bővíthető is.
A hitelesítő rendszerek közös jellemzője, hogy a hitelesítési procedúrát két fázisra bontják. Egy kezdeti
hitelesítési fázisra, amikor például bekéri a név/jelszó párt. Miután ezeket megfelelőnek találta a
hitelesítés szolgáltató, létrehoz egy kódolt adatsorból álló kulcsot (tokent, jegyet, bélyeget), és ezt
visszaküldi a kliensnek. A kliens a következő kapcsolat felépítésekor a kapott kulcsot (és nem a
név/jelszó párt) küldi a hitelesítést felügyelő rendszernek, ami ez alapján hitelesnek fogja találni az
aktuális kapcsolatfelvételi kérelmet és tájékoztatja a kiszolgálót erről. Ez utóbbi a második fázis, ami
nem egyszeri, hanem az adatkapcsolati protokolltól függően rendszeresen ismétlődik (form alapú
hitelesítésű webalkalmazásnál minden request esetén). Ezeknek a kódkulcsoknak egy gyakori
jellemzője, hogy véges érvényességi idejük van. A szakaszos kommunikáció miatt folyamatosan le-
felkapcsolódó kliensnek ezért ezt rendszeresen meg kell újítania, mielőtt lejárna az érvényességi
határideje. A határidő lejárta a hitelesítettség végét is jelenti, aminek következménye, hogy a kliens a
kezdeti hitelesítési fázisba kerülve újra, név és jelszó megadására kényszerül.
Előre kell bocsájtani, hogy az MVC framework nem rendelkezik saját hitelesítési rendszerrel. Remélem
ez inkább meglepetést, mint csalódást okozott. Valójában nincs is rá szüksége, hiszen az ASP.NET
alaprendszernek van egy kiforrott, komplett infrastruktúrája erre a feladatkörre. Az erre ráépülő MVC-
nek elég, ha ezt felhasználja. Emiatt akinek ismerős az ASP.NET hitelesítési rendszere, annak az MVC
9.3 A biztonság és az értelmes adatok - Felhasználó hitelesítés 1-246
nem sok újdonsággal fog szolgálni ezen a téren. Az MVC framework-re a hitelesítés során annyi feladat
hárul, hogy a saját szisztémáján keresztül hidat állítson az ASP.NET hitelesítési szolgáltatásaihoz. A
következőkben ezeket az ASP.NET-re épülő réteget fogjuk az MVC felől megvizsgálni.
Ennek a hitelesítési módnak a lényege, hogy a felhasználót egy bejelentkezési weboldalra irányítva
elkérjük a felhasználó nevét vagy email címét és a jelszavát. Ezeket megkeressük a rendszerben, és ha
találunk egyezőt, onnantól a felhasználót hitelesítettnek tekintjük, és a böngészőnek kimegy egy
autentikációs token. A név/jelszó párost nem kérjük el minden új oldalra navigáláskor, hanem az
ASP.NET-től kapott tokent a böngésző visszaküldi minden új oldal lekéréskor. A tokent két helyen
tudjuk tároltatni a böngészővel, a cookie-ban és az URL-ben. Mindkettő elég alacsony biztonsági
szinttel rendelkezik egy ilyen kényes információ számára. Nem beszélve arról, hogy a kezdeti
hitelesítési fázisban, a bejelentkezéskor, a név és a jelszó egy form input mezőin keresztül szöveges
formában érkeznek a szerverhez. Emiatt az ilyen hitelesítésű oldalakat célszerű SSL titkosított
csatornán elérhetővé tenni. Ha mást nem is legalább a bejelentkezési actiont.
Az MVC Internet projekt sablon által generált alkalmazás tartalmazza ezt a hitelesítési módot kiszolgáló
infrastruktúrát. Vajon mit tud?
Hitelesítési tokenek
Nézzük ezt meg egy kicsit alaposabban egy Chrome böngésző beépített fejlesztői eszközeivel (press
F12). (Hasonló képességekkel a FireFox+FireBug is rendelkezik)
Lépjünk ki a Log off-al és a „Resources” fül alatt a Cookies ágban nyissuk ki a localhost-ot és ha itt
találunk bármit is, töröljük ki. (Sor kiválasztás és a sorok alatt az X ikonnal, ahogy a nyíl jelzi).
Ezután frissítsük az oldalt (press F5). Nálam az alábbi jelent meg, ez lesz a kiinduló alap:
A „_lastproduct” ismerős lehet, mert ez az AntiForgery cookie-ja, a nevét mi állítottuk át pár fejezettel
előbb: System.Web.Helpers.AntiForgeryConfig.CookieName = "_lastproduct"; . Tehát a
bejelentkezési oldal használja az Html.AntiForgeryToken-t.
Most, ha bejelentkezünk, akkor megjelenik egy .ASPXAUTH nevű cookie, aminek a lejárati ideje
(Expires): Session, ami most böngészési munkamenetet jelent és nem a szerver Sessiont. Ennek külön
cookie-ja lenne „ASP.NET_SessionId” néven és akkor jelenne meg, ha az oldalak kiszolgáló kódja
valahol már igénybe vette volna a Session objektumot.
Az .ASPXAUTH cookie lejárati ideje konkrét időzóna-független időpont lesz, ha a „Remember me”
checkboxot bepipáljuk. Így a hitelesítési token megmarad a böngésző bezárása után is.
Ez a token érvényes marad az alkalmazás/webszerver újraindítása után is. Sőt másik számítógépre
másik böngészőbe is át lehet másolni, akkor is működni fog. Ez egy komoly biztonsági kérdés, mert
nem is szükséges a név/jelszó ismerete, ahhoz, hogy egy illetéktelen személy be tudjon jelentkezni az
oldalunkra. Ez az autentikációs cookie, hasonlóan a bejelentkezési név – jelszó szöveges tartamára egy
hálózat figyelővel (is) ellopható. Emiatt két szigorítási szabályt lehet tenni. Az egyik a már javasolt SSL
titkosítás előírása az egész alkalmazásra. Ez mondjuk a tárolt cookie-nak nem számít. Ezért egyes
vállalatoknál központilag letilthatják a cookie-k böngésző oldali tárolását, ez a másik szabály. Ekkor az
alkalmazásunk és a felhasználó is meg lenne lőve. A felhasználó azért, mert minden új oldalra
látogatáskor meg kéne hogy adja újra a nevét és jelszavát. Emiatt az ASP.NET (és MVC) képes a cookie
helyett az URL-ben tárolni a tokent. A hasznos URL path elé beszúrja a tokent ilyen formátumban:
9.3 A biztonság és az értelmes adatok - Felhasználó hitelesítés 1-248
http://localhost:18005/(F(XYzGzk6LdrVhPSmk55FrMlkmYVGuX1px9Iz1Nkw_5HGO9seOsLZeOEE4FW
2dZL4XjLYkd_6wD8OawjuoP-1SW9lB_id_qXHFSGXT42c5YPXIJ9vV0-
m2oDdnaVaSSyu5GzYmuo2JLDuztGOig4bVoIW3-SOZrmfLsJnnMsSHGKY1))/Home/About
<system.web>
<authentication mode="Forms">
<forms loginUrl="~/Account/Login" timeout="2880" cookieless="UseUri"/>
</authentication>
loginUrl - Az a relatív URL, ahol a felhasználót a rendszer be tudja jelentkeztetni. Majd látni fogjuk,
hogy action szinten meg tudjuk adni, hogy az adott action csak hitelesített felhasználóknak legyen
elérhető. A nem hitelesített felhasználókat automatikusan a loginUrl oldalra irányítja az ASP.NET. Ez
MVC framework esetén az Account kontroller Login actionje lesz.
timeout – cookie-ban tárolt token percekben megadott lejárati ideje, ha a felhasználó nem használja
az oldalakat (idle time). Az alapértelmezett értéke: két nap. A timeout értelmezésében van egy csavar,
amit a rendszer némi erőforrás takarékosság miatt használ. Az lenne kézenfekvő működés, hogy
minden új oldallekérés esetén ez a lejárati idő kitolódik a timeout-ban beállított értékkel. De ez csak a
cookie kibocsájtási idejéhez képest a lejárati idő fele után történik meg. Csak ekkor kap új lejárati idővel
rendelkező auht. tokent. Tehát, az alapértelmezett értéket véve a bejelentkezés után, azaz
logintime+23:59:00 perccel letöltve az oldalt, még megmarad a lejárati idő és logintime+48:00:00 óra
múlva lejár. Azonban, ha a felhasználó a bejelentkezés után 24:01:00 óra múlva újra lekéri az oldalt,
akkor új cookie-t kap, ami még két napig érvényes lesz (logintime+24:01:00+48:00:00).
cookieless – A token tárolási módja. Az UseUri működését már láttuk. Van még az AutoDetect, ami
kiküld egy AspxAutoDetectCookieSupport nevű cookie-t és ezzel megvizsgálja, hogy engedélyezve van-
e a böngészőben cookie kezelés. (Ha nem kapja vissza, akkor nincs). Lehet még „UseCookies”, ekkor
mindig cookie-ba kerül a token. A „UseDeviceProfile” az alapértelmezett értéke, ilyen beállítás mellett
az ASP.NET a beérkező requestben szereplő böngészőnév (User-Agent) alapján, egy táblázatból veszi,
hogy használjon-e cookie-t vagy sem.
requireSSL – Ezzel előírhatjuk, hogy a bejelentkezés csak SSL titkosítással történhet. Mivel ez egy
ASP.NET-re is érvényes beállítás, helyette használhatjuk az MVC-s attribútumot is.
name – A cookie nevét adhatjuk meg ezzel, ha az alapértelmezett ".ASPXAUTH" név helyett mást
szeretnénk neki adni.
9.3 A biztonság és az értelmes adatok - Felhasználó hitelesítés 1-249
Membership providerek
Annyit láttunk eddig, hogy van egy több szempont alapján is szabályozható alap hitelesítési
rendszerünk. A kérdés most az lenne, hogy vajon hol tárolódnak a regisztrációs adatok és hogyan megy
végbe a hitelesítés? Ehhez nézzük meg az MVC projekt AccountController osztály Login actionjeit.
[Authorize]
[InitializeSimpleMembership]
public class AccountController : Controller
{
[AllowAnonymous]
public ActionResult Login(string returnUrl)
{
ViewBag.ReturnUrl = returnUrl;
return View();
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public ActionResult Login(LoginModel model, string returnUrl)
{
if (ModelState.IsValid && WebSecurity.Login(model.UserName, model.Password,
persistCookie: model.RememberMe))
{
return RedirectToLocal(returnUrl);
}
Működése legalább annyira egyszerű, mint amilyen nagy a védelmi haszna. Ha a returnURL az
9.3 A biztonság és az értelmes adatok - Felhasználó hitelesítés 1-250
alkalmazásunk belső relatív oldala (isLocalUrl), akkor működhet a redirect, ha nem akkor megyünk a
nyitólapra, mert vélhetően támadási kísérletről van szó.
A Login post actionje pedig mint a karácsonyfa, fel van díszíve attribútumokkal. Szerintem mindegyik
ismert már. A belsejében van egy validációs állapotellenőrzés, hogy van-e kitöltve név és jelszó. (A
validációs szabályok a LoginModel osztályban vannak a propertyken). Utána következik a (WebMatrix-
ból érkezett) WebSecurity hitelesítés szolgáltató felhasználásával: a bejelentkeztetés.
bool EnablePasswordRetrieval
bool EnablePasswordReset
bool RequiresQuestionAndAnswer
int MaxInvalidPasswordAttempts
bool RequiresUniqueEmail
int MinRequiredPasswordLength
string PasswordStrengthRegularExpression
bool ChangePassword(string username, string oldPassword, string newPassword);
string ResetPassword(string username, string answer);
void UpdateUser(MembershipUser user);
bool ValidateUser(string username, string password);
MembershipUser GetUser(string username, bool userIsOnline);
string GetUserNameByEmail(string email);
A felsorolás még csak kb. a fele a lehetőségeknek, nem is volt célom, hogy részletezzem. A propertyk
és metódusok nevei elárulják, hogy mire valóak, és látható, hogy az általános igényeket elég jól lefedő
felületről van szó. A MembershipProvider megvalósításunkat kell felkínálni az ASP.NET (+MVC)
számára és ez egy hidat fog képezni az ASP.NET hitelesítési rendszere és a konkrét felhasználói adatok
tárolása, jelszó titkosítási módszerünk és validálási logikánk között. A konkrét megvalósításban
tudathatjuk az ASP.NET –el, hogy van-e jelszó visszaállítási képessége, mennyi hibás jelszópróbálkozás
megengedett, az erős jelszót milyen reguláris kifejezéssel tudja validálni, stb. Az MVC 4 előtt, legalábbis
nálam, minden alkalmazásfejlesztés úgy indult, hogy a beépített membership providert le kellett
cserélni, mert valahogy sosem volt jó a beépített merev, SQL alapú AspNetMembership providere. A
helyzet sokat javult az MVC4-ben, még akkor is, ha a provider neve úgy kezdődik, hogy „Simple” és
ennek megfelelően elég egyszerű. De lehet, hogy azt akarták sugallni a nevével, hogy egyszerű
bővíteni...
WebSecurity.InitializeDatabaseConnection(
"DefaultConnection", "UserProfile", "UserId", "UserName", autoCreateTables: true);
}
catch (Exception ex)
{
throw new InvalidOperationException("The ASP.NET Simple Membership database could not be
initialized. For more information, please see http://go.microsoft.com/fwlink/?LinkId=256588", ex);
}
}
}
<connectionStrings>
<add name="DefaultConnection" connectionString="Data Source=(LocalDb)\v11.0;Initial Catalog=aspnet-
MvcApplication1-20130224142421;Integrated Security=SSPI;AttachDBFilename=|DataDirectory|\aspnet-
MvcApplication1-20130224142421.mdf" providerName="System.Data.SqlClient"/>
</connectionStrings>
InitializeDatabaseConnection("DefaultConnection",
"UserProfile", "UserId", "UserName", autoCreateTables: true);
Az első paramétere szintén a connection string nevét jelenti a web.config-ból. Utána a felhasználói
profilt tároló tábla neve és annak két nélkülözhetetlen mezőneve következik: a felhasználói azonosító
és a felhasználói név. Az eddigiekből látszik, hogy a UserProfile (vagy ahogy nevezzük) tábla struktúrája
és tartalma jórészt ránk van bízva. Ez a két mező kell bele kötelezően, de bővíthetjük, amivel akarjuk.
Természetesen nem muszáj azt az inicializálási metodikát követnünk, amit az
InitializeSimpleMembership action filter biztosít számunkra, főleg, ha tudjuk, hogy az alkalmazásunk
erősen hitelesítés függő. Ez alatt azt értem, hogy az oldalak jelentős része csak bejelentkezés után
érhető el. Ekkor az attribútumot kidobhatjuk és az kezdeti beállítás logikáját áttehetjük a global.asax
Application_Start eseményébe is. A lényeg, hogy az InitializeDatabaseConnection az első
autentikációs kísérlet előtt lefusson, mert ellenkező esetben a régi SqlMemeberShipProvider fog
működni, aminek a használata elég körülményes (szerintem). Nincs előírva az, hogy Entity Framework-
öt kellene használnunk a UserProfile profil tábla tartalmának kezelésére. Mivel a WebSecurity saját
adatkezelési rétegen keresztül éri el profil táblát és számára csak a tábla és a két mező neve kell, emiatt
nem vagyunk kötve az EF-höz. Lehet hagyományos ADO.NET is, amivel létrehozzuk és kezeljük a
kiegészítő mezőadatait. És ezt is kell tennünk, úgy értve, hogy a profil tábla plusz mezőjének kezelését
9.3 A biztonság és az értelmes adatok - Felhasználó hitelesítés 1-252
nem biztosítja a WebSecurity. Ha történetesen ebben telefonszámot, email címet, stb. tárolunk, akkor
azt nekünk kell lekérdeznünk. A WebSecurity-től csak a bejelentkezett UserId és a UserName tartalmát
lehet megtudni. (+ a bejelentkezettség állapotát)
int userid=WebSecurity.CurrentUserId;
string userName = WebSecurity.CurrentUserName;
bool isAuth = WebSecurity.IsAuthenticated;
[Table("UserProfile")]
public class UserProfile
{
[Key]
[DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
public int UserId { get; set; }
[StringLength(60)]
public string UserName { get; set; }
}
Ahogy az szokott lenni, egy jelszó visszaállító linket tudunk készíteni. Az email-ben kiküldjük az URL-t
benne a tokennel és a linket fogadó actionben ellenőrizni tudjuk, hogy melyik felhasználó számára lett
kiküldve a token a GetUserIdFromPasswordResetToken(string token) metódussal, aminek a
visszatérési értéke a UserId. Esetleg az actionben bekérhetjük az új jelszót és a ResetPassword(string
passwordResetToken, string newPassword) metódussal tudjuk beállítani egy menetben.
megerősítést váró levéllel. A jóváhagyási emailben levő URL actionjében végül, a felhasználó
létrehozási procedúrát le tudjuk zárni a ConfirmAccount(string accountConfirmationToken)
metódussal. Ez után már be fog tudni jelentkezi a felhasználó. A jóváhagyási státuszt név alapján tudjuk
lekérni az IsConfirmed(string userName) segítségével.
Nem érdemes körülnézni, hogy milyen lehetőségeket rejt még a WebSecurity a szerepek/csoportok
kezelésére, mert ezzel a végére is értünk a sornak. Ráadásul az Internet Application projekt template
nem tartalmaz kezelőfelületet a jogosultság csoportok kezeléséhez. Viszont a WebSecurity
inicializálása után elérhető a System.Web.Security.Roles osztály és ennek Provider metódusa, ami
ebben az esetben egy SimpleRoleProvider.
[HttpPost]
public ActionResult RoleEdit(RoleModel model)
{
SimpleRoleProvider simpleRoles = Roles.Provider as SimpleRoleProvider;
var users = simpleRoles.GetUsersInRole(model.PrevName);
simpleRoles.RemoveUsersFromRoles(users, new string[] { model.PrevName });
simpleRoles.DeleteRole(model.PrevName, false);
simpleRoles.CreateRole(model.Name);
simpleRoles.AddUsersToRoles(users, new string[] { model.Name });
return RedirectToAction("RoleList");
}
A form hitelesítéshez tartozik, hogy a hitelesítési rendszer mélyén van még egy lehetőségünk a
beavatkozásra. Erre a FormsAuthentication osztály néhány metódusa ad lehetőséget. Ezzel több
9.3 A biztonság és az értelmes adatok - Felhasználó hitelesítés 1-254
SignOut() – Valójában a kibocsájtott autentikációs cookie-t érvényteleníti azzal, hogy egy 1999.10.12
43
-ben lejárt dátumú cookie-ra cseréli le. Ezzel gyakorlatilag kijelentkezteti a felhasználót.
Ezen kívül még van néhány propertyje, amivel a web.config-ban levő <forms > elemben meghatározott
beállításokat tudjuk elérni.
Az MVC és a hitelesítés
Az action filterekkel foglalkozó fejezetben (5.9) megismertük a beépített Authorize attribútumot, most
nézzük meg mire jó és mire nem. Azt láttuk, hogy, ha megtalálható egy actionön vagy kontrolleren
(vagy globálisan), akkor a felhasználót meginvitálja egy bejelentkezésre, ha még eddig nem tette meg.
Ezen túlmenően előírhatjuk, hogy egy vagy több csoporthoz is tartoznia kell az action eléréséhez:
Sőt lemehetünk felhasználói szintre és azt is megszabhatjuk, hogy csak a felsorolt felhasználók
férhessenek hozzá:
Ennek csak akkor van értelme, ha valamilyen rendszerszintű felhasználó van definiálva, mert a normál
felhasználói neveket nem szokták a jogosultságot kezelő kódba belevarrni. Jogosultsági hiány esetén
egy HttpUnauthorizedResult http hibával reagál. Mindkettő paraméterezésének (Users és Roles)
furcsasága, hogyha egy ilyen actionhöz navigálunk és nem vagyunk benne a felsorolt role-ban, vagy a
felhasználói nevünk nem szerepel a felsorolásban, akkor a bejelentkezési oldalra navigál. Ilyenkor egy
„Nincs jogosultságod” jellegű üzenetet várnék. Sebaj, mivel az Authorize attribútum több virtuális
metódussal rendelkezik, könnyen származtathatunk belőle és elkülöníthetjük a „nincs bejelentkezve”
és a „bejelentkezett, de nem tagja a role-nak” helyzeteket. A gyári attribútum egy kalap alá veszi a
kettőt, de az alábbi megvalósítás egy HTTP redirekcióval reagál arra, ha a felhasználó a felsorolt
szerepek közül egyiknek sem tagja.
43
Misztikus dátum. http://stackoverflow.com/questions/701030/whats-the-significance-of-oct-12-1999
9.3 A biztonság és az értelmes adatok - Felhasználó hitelesítés 1-255
Az attribútum Roles stringjét szétdarabolja a vesszők mentén, és ha a felhasználó nem tagja a role-
nak, akkor a filter Result értékét feltölti egy RedirectResult-al. A lényegi vizsgálatot a HttpContext-en
elérhető IPrincipal metódusa végzi el, ami rendelkezik még két fontos információval. A bejelentkezett
felhasználó nevével és hogy van-e hitelesítve vagy nincs. Néha ez is elég, ha kódból akarunk döntést
hozni.
Nagyjából ennyire képes a form alapú hitelesítés és ekkora a támogatottsága az MVC részéről.
<configuration>
<system.web>
<authentication mode="Windows" />
A web szerveren a működéhez további beállításokra is szükség van. Az IIS mostani verzióiban például
alapértelmezetten ki van kapcsolva a Windows alapú hitelesítés.
Miután elkészült a projekt elég elővenni két fájlt. A web.config-ot, hogy megnézzük, tényleg ott van a
mode=”Windows” beállítás. Mellette egy readme.txt fájlt találunk a projekt gyökerében, amiben le van
írva, hogy miket kell állítani a web szervereken (IIS és IIS Express), hogy úgy működjön, ahogy
szeretnénk. Ezt most nem másolnám ide, csak némi kiegészítést adnék hozzá.
A webszerver beállítása után indíthatjuk a projektet. A nyitólapon a jobb felső sarkokban ott kéne
lennie a Windows bejelentkezési névnek:
Azért csak „kéne”, mert ez függ attól is, hogy a gépünk Windows
tartományba van-e léptetve, és hogy milyen böngészővel
nézzük az oldalt, és hogy a projektünk az URL-je szerint
localhost-on fut-e. Ha a gépünk csak munkacsoport tag (home
group, workgroup), és Internet Explorerrel dolgozunk és a
projektünk URL-je a localhost, ami tipikus eset, akkor szükségessé válik, hogy állítsunk az IE Security
fülön. Azt kell elérni, hogy elfogadja azt hogy a „localhost” az intranet része. Máskülönben név/jelszó
bekérő ablakkal fog zaklatni minket. Internet Options->Secutiry(tab) :
9.3 A biztonság és az értelmes adatok - Felhasználó hitelesítés 1-257
Utána Close->OK. Ezen kívül még szükséges lehet a Security level állítása is. A Custom level gombbal
elérhető beállítások kötött az Automatic logon only in Intranet zone segítségével elkerülhetjük a
rendszeresen megjelenő jelszóbekérő ablakot:
System.Security.Principal.WindowsPrincipal user =
(System.Security.Principal.WindowsPrincipal)HttpContext.User;
System.Security.Principal.WindowsIdentity identity =
(System.Security.Principal.WindowsIdentity)user.Identity;
9.3 A biztonság és az értelmes adatok - Felhasználó hitelesítés 1-258
A nagy közösségi oldalak, a Google, Yahoo, FaceBook jelenleg úgy jelennek meg az Internet
szempontjából, mint létfontosságú csomópontok. Nagy a valószínűsége, hogy egy átlag
internetfelhasználónak van felhasználói fiókja valamelyik nagy rendszerben. Egy másik tény, hogy a
felhasználókat legjobban négy dolog bosszantja. Amikor (számukra) új website-ot keresnek fel: és lassú
az oldalbetöltés, ha áttekinthetetlen az oldal, ha regisztrálni kell ahhoz, hogy valami számukra fontosat
meg tudjanak tenni, és ha ezzel kapcsolatosan egy új felhasználói nevet és jelszót kell megjegyezniük.
Ez utóbbi kettőre ad segítséget az OAuth és az OpenID technológia, azzal, hogy az ilyen nagy
webszolgáltatók saját hitelesítési rendszereit használhatjuk fel a saját webalkalmazásunkhoz. Röviden
arról van szó, hogy a felhasználói nevet, a jelszókezelést, és a bejelentkeztetést például a Google
biztosítja, a hitelesítés tényéről pedig egy autentikációs csomagot küld át a mi alkalmazásunknak.
Ennek a csomagnak általában a lényegi tartalma, a felhasználó neve és egy token (mi más is lehetne).
Ezt a felhasználói nevet/azonosítót aztán összerendelhetjük a mi rendszerünkben tárolt felhasználói
jellemzőinkkel, mint például a helyi UserId-vel és role tagsággal. Érdemes tudni azonban, hogy ez a
hitelesítési képesség egy NuGet csomagból jön és nem szerves része az MVC frameworknek, és
elérhető volt már az MVC3-ban is.
Az MVC Internet projekt template készen tartalmazza ezt a hitelesítési módozatot, egyes esetekben
elég csak kivenni a kommenteket és már működik is. Az App_Start mappában az AuthConfig.cs-ben
találhatóak a hitelesítés szolgáltatásokhoz kapcsolódó regisztrációs metódusok. Példaként eltávolítva
a kommentet az OAuthWebSecurity.RegisterGoogleClient(); regisztráció elől, az alkalmazásunk már
képes is működni úgy hogy a tényleges hitelesítést a Google végzi el. Innentől, ha a login oldalra
megyünk megjelenik a Google gomb:
A Form alapú hitelesítésnél megnéztük a SimpleMemberShipProvider által használt táblákat és már ott
is látszott, hogy létezik a webpages_OAuthMembership nevű tábla. Az előbb végiglépkedtem a Google
regisztrációs folyamaton, ennek eredménye mindössze egy sor ebben a táblában.
A UserId szintén 1:1 kapcsolatot biztosít a UserProfile (vagy ahogy nevezzük) táblával és a
webpages_Membership táblával is, ha a Manage Account oldalon rögzítünk egy jelszót. Mellesleg, ha
rögzítünk jelszót az AccountControllerben levő logika biztosítani fog egy Remove
gombot a Google fiók mellé, amivel leválaszthatjuk a helyi profilt a Google fióktól
(mivel már nem árvul el a helyi profil bejegyzés)
A Google hitelesítést „beüzemelni” rém egyszerű. A többi szolgáltató igényli, hogy az ő rendszerükben
regisztráljuk az alkalmazásunkat. Ehhez kibocsájtanak egy API kulcsot, ami azonosítja az ő
rendszerükben az alkalmazást és egy tokent, ami a jelszó szerepét tölti be.
Hasonlóan a Form alapú hitelesítéshez a helyi UserProfile táblát tudjuk bővíteni és ebben tárolni helyi
érdekű adatokat. De akár át is vehetünk a külső hitelesítés szolgáltatóból adatokat, profil képet. Persze
ez nem egyszerű, mert a szolgáltató API-ját kell felhasználni. Az MVC5-ben elvileg lesz ilyen API kliens,
a Facebook-hoz.
Próbálgassuk egy kicsit ezt a hitelesítési módszert. Az előbb végigkövetett regisztrációs lépések végén
megjelent egy ablak, amiben a helyi felhasználói nevet kérdezte meg profil. Ez a Google esetén az email
cím. Azonban nem olyan barátságos és spam veszélyes is, hogy kiírjuk a felhasználó email címét.
Próbáljunk ezen javítani egy kicsit.
9.3 A biztonság és az értelmes adatok - Felhasználó hitelesítés 1-260
public GoogleWithFullNameClient()
: base("Google", WellKnownProviders.Google) { }
A megszerzett fullname pedig a View modellbe csomagolva megjelenik, mint alapérték a regisztrációs
űrlapon.
Ahhoz, hogy működjön a saját kliensünk a normál Google kliens regisztrációját ki kell kommentezni, és
az általunk leszármaztatott típust beregisztrálni.
//OAuthWebSecurity.RegisterGoogleClient("Google");
OAuthWebSecurity.RegisterClient(new GoogleWithFullNameClient(), "Google”, null);
Ezzel a módszerrel a szolgáltató által az API-ján elérhető összes információt át tudjuk emelni az
alkalmazásunkba. Némelyik specifikus és nem szerepel a WellKnownAttributes-ok között. Ezeket a
szolgáltató API dokumentációjában lehet megtalálni.
Ezzel végére értünk a beérkező request feldolgozási sorában a hitelesítésre használható periódus
átnézésével. A következőkben a request útja folytatódik és némileg annak adattartalmára állítjuk a
védelem fókuszát.
Ha megnézzük a Visual Studio scaffold Edit template-el generált cshtml fájlt, ott találunk benne egy
hidden HTML input mezőt, ami a modell objektum azonosítót hordozza, például egy „Id” néven. A
hidden mező pedig a POST során visszakerül a szerverre. Az Edit Action metódusban, ami fogadja a
requestet, ott szokott lenni egy modell validátor, némi ellenőrzés, ahogy az kell, majd jön a modell
feldolgozása, aztán a modell tartalmának befrissítése az adatbázisba. Végül egy sql „update Táblanév
set mezőnév=valami where Id=azonosító” fog lefutni az SQL szerveren. Nézzük meg alaposan, hogy
ebben az esetben a where feltételben szereplő sor egyedi azonosítója, honnan is származik? Hát a mi
9.4 A biztonság és az értelmes adatok - Kódolt azonosítók 1-262
A következőkben tárgyalt példák során ilyen titkosított adatokat fogunk küldözgetni oda-vissza a
böngésző és a szerver között. Mindemellett a megvalósításokba igyekeztem olyan eddig megismert
témákat is belevenni, ami életszerűbb helyzetben mutatja meg azokat. A példakódok a
Controllers/Securities mappában leleddzenek.
A felesleges hidden mezőkről lesz szó. Valahol az emberi viselkedésünkben az van mélyen, hogy ami
rejtett az egyből izgalmassá válik. A gyerekeken lehet ezt jól megfigyelni. Például, ha azt mondjuk, hogy
abba a fiókba ne nézzen bele, mert titkos és tilos, akkor ha csak nem valami igen jól nevelt gyerek,
biztosak lehetünk benne, hogy foglalkozni fog a dologgal és legjobb esetben csak az álmában fogja
nyitogatni. A rejtegetés, motivációt ad a kalandos felfedezésre. Ezek után megkérdezem: kell
egyáltalán több mint egy hidden mező? Válasz, hogy igazából nem. Minden egyes plusz, rejtett,
titokzatos mező egy picit lágyítja a pajzsot. Néha az is kimarad a kódból, hogy ezt is ellenőrizni kéne,
és csak úgy elfogadjuk az értékét.
Néhány fejezettel előbb az AntiForgeryToken-nél láttuk, hogy van arra mód, hogy a tokenbe kerülő
adathoz hozzácsapjunk valami saját stringet is, amit a válaszba visszakapunk. Akkor egy statikus „Sós
mókus” és egy oldal sorszám volt a tárolt adat. Azonban ezt, korlátozottan ugyan, de bővíthetjük is. (A
korlátot a kódolt szöveg hossza jelenti elsősorban) Az alábbi példakódokban egy „hiddenid” nevű
(feltételezett) entitásazonosítót utaztatunk meg az AntiForgery rendszerrel. Egy trükköt kell
alkalmaznunk, mivel az action és a kontroller egyik adata sem érhető el az
AntiForgeryAdditionalDataProvider-ben csak a RouteData tároló. Ez az, ami tárolja az URL-ben levő
szakaszoknak megfelelő a route bejegyzésben definiált értékeit (action, controller, id általában). Mivel
a detail és edit oldalak amúgy is kihasználják a route Id bejegyzését, mint entitásazonosítót, így a
RouteData tároló erre megfelelő hely lesz.
A get requestet kiszolgáló oldal és action most az Index lesz (ez tipikus esetben egy Details nevű action
szokott lenni). Itt elteszünk a RouteData-ba egy 12-es azonosítót.
public SecurityController()
{
System.Web.Helpers.AntiForgeryConfig.AdditionalDataProvider =
new HiddenAFDataProvider();
}
Egy másik actionben elvárjuk, hogy a form posttal együtt megkapjuk metódusparaméterként. Ezt
megtehetjük, hiszen láttuk, hogy a model binder által használt RouteDataValueProviderFactory fog
9.4 A biztonság és az értelmes adatok - Kódolt azonosítók 1-263
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult AntiForgeryServed(int hiddenid)
{
return Content("Minden rendben "+hiddenid);
}
mvchandler.RequestContext.RouteData.Values.Add("hiddenid", additionalData);
return true;
}
}
Beadandó házi feladatnak pont megfelelő lesz, ha ezt a folyamatot átülteti az olvasó egy JSON oda-
vissza adatcserébe.
9.4 A biztonság és az értelmes adatok - Kódolt azonosítók 1-264
Ha az előbbi megoldás elsőre túl nagy falatnak tűnt, nézzünk egy kicsivel egyszerűbbet (vagyis még
összetettebbet), amikor is egy saját titkosított HTML hidden mezőt gyártó Html extension-t használunk
fel a titkosítandó adatokhoz. Itt az alkatrész lista, ami szükséges a teljes működéshez:
Kell egy…
Kóder-dekóder, ami a hidden mezőbe kerülő adatot titkosítja és visszaalakítja. Ennek object-
>string és visszafelé string->object átalakításokat is kell végeznie, mivel a HTML-be csak
szöveges értéket tehetünk.
Html helper a hidden mezőhöz. Ez fogja hordozni a kódolt szöveget.
ValueProviderFactory, amit a model binder majd használatba vesz. Ez fogja a post során érkező
a hidden mezőben levő titkosított stringet visszaalakítani. Majd szolgáltatni a modell
propertykhez vagy az action paraméterekhez a típusos adatokat.
Modell, amivel ki tudjuk próbálni.
Azért, hogy a model binder sajátosságait és az objektum fa bejárását is gyakorolhassuk, egy olyan
képességgel is felruházzuk ezt a megvalósítást, hogy ne csak primitív típusokkal (pl. int típusú Id) tudjon
dolgozni, hanem egyből tárolhassunk is egy komplett osztálypéldányt is a hidden mezőben.
static StringEncoderHelper()
{
Rijndael = new RijndaelManaged
{
Mode = CipherMode.CBC,
Padding = PaddingMode.PKCS7,
KeySize = 128, BlockSize = 128, Key = SecurityKey, IV = InitVector
};
}
A következő alkatrész a Html helper. A rövidség kedvéért ez egy alap verzió, nem a lambda kifejezéses
…For változat, ami most feleslegesen elbonyolítaná a kódot.
public static MvcHtmlString EncodedHidden(this HtmlHelper htmlHelper, string name, object value)
{
if (value == null) return MvcHtmlString.Empty;
Az oldal előállításáért még mindig az előző részben is szereplő Index action metódus felel, kicsit
bővítve, mert egy modellt is kell példányosítani:
A modellben nincs semmi különös. Van egy statikus generátor metódusa és egy ToString override-ja,
hogy megjelenítse a saját tartalmát. Alatta van még egy modell, ami hordozza az első modellt egy
propertyjében. Ez kell majd ahhoz, hogy lássuk, hogy a hidden mezőben teljes objektumot is tudunk
tároltatni nem csak primitív típusokat.
9.4 A biztonság és az értelmes adatok - Kódolt azonosítók 1-266
Az előbbi osztály őse következik, ahol van egy AddToProvidedList metódus. Abban van szerepe, hogyha
9.4 A biztonság és az értelmes adatok - Kódolt azonosítók 1-267
a property egy osztályt hordoz, akkor annak a propertyjeinek az elérési útját tudjuk szolgáltatni
rootproperty.childproperty formában. A model binder ilyen formában igényli.
Mint minden rendes extra value providert, így ezt is be kell regisztrálni a global.asax-ban. Nem elég
csak hozzáadni, hanem előre kell tenni, mert a regisztrációs listában a FormValueProviderFactory-t
meg kell előznünk. (ld.: 8.4 fejezet: Mélyen belül). Mivel annak a gyári megvalósításnak lenne a feladata
a hidden mezők feldolgozása. Így elorozzuk előle a „hobj-” kezdőnevű mezők feldolgozását, mert úgyse
rá tartozik.
Első nekifutásra próbáljuk ki két egyszerű típussal. Ahhoz, hogy az új frissen sült Html.EncodeHidden
bővítő metódusunkat használni tudjuk, be kell emelni a View elejére a névterét (vagy be kell tenni a
Views/web.config-ba). A modell két tulajdonságát kódoltatjuk el (Hid és HGuid).
@using MvcApplication1.Controllers.Securities
@model SecurityModel
@Html.TextBox("FullName", Model.FullName)
<input type="submit" value=" Ment " />
}
9.4 A biztonság és az értelmes adatok - Kódolt azonosítók 1-268
A View renderelt eredményén látszanak a hidden mezők név prefixumai és hogy ott vannak a value
attribútumban a kódolt adatok.
A post requestet fogadó action SecurityModel típusú paramétere helyesen feltöltve érkezik.
[HttpPost]
public ActionResult EncodedHidden(SecurityModel model)
{
return Content(model.ToString());
}
Minden rendben, mert működött két egyszerű típussal, de most próbáljuk ki osztály típussal is. Íme,
egy View hogy használni tudjuk az új helpert:
@Html.Display("FullName")<br />
@Html.Display("HId")<br />
<input type="submit" value=" Ment " />
}
A hozzá tartozó action paramétere az a bizonyos másik modell, aminek csak egy alpropertyje a normál
SecurityModel, amit beágyaztunk a hidden inputba:
[HttpPost]
public ActionResult EncodedInternalHidden(SecurityStorageModel model)
{
return Content("Név: " + model.SecurityModelInternal.FullName +
" Hid: " + model.SecurityModelInternal.HId);
}
Tartozom még egy magyarázattal, hogy miért a JavaScriptSerializer-t használtam és miért nem valami
mást, mondjuk bináris vagy XML sorosítót. Az objektum visszasorosításakor van két összefüggő
dilemma. Az egyik, hogy a beérkező kódolt stringről nem tudjuk, hogy milyen konkrét típust hordoz.
Így nagyon nehéz lenne példányosítani a visszasorosításakor. A másik, hogy igazából nem is a teljes
objektumra van szükségünk, hanem csak annak a propertyjeire és értékeire név szerint. Így nincs is
szükségünk a model típusára és példányára sem. Az alábbi ábrán a visszasorosítás utáni szótár elemei
láthatóak property név-érték párokkal.
9.4 A biztonság és az értelmes adatok - Kódolt azonosítók 1-269
Azonosítók az URL-ben.
Az URL-t, mint erőforrás azonosítót csak akkor érdemes elkódolni, ha az tényleg tartalmaz
entitásazonosítót is. A katalógusokat, listákat mutató oldal URL-je jó, ahogy van. Ilyen elkódolt
azonosítókra számos példát találunk, bármerre nézünk a neten.
A következő példasorozat is ezt a fokozott biztonságú URL kódolást valósítja meg. A titkosítást és
karakterkódolást ugyanaz a StringEncoderHelper fogja végezni, mint amit a hidden mezőknél
használtunk. Most két Html helpert is készítettem. Az első csak egy int típusú Id-t képes kezelni. Végül
is ez az alapcél. A második viszont több értékkel is boldogul a RouteValueDictionary-n keresztül.
Normál esetben ez utóbbi név-érték tartalmából lesznek a query string-ek név-érték párjai. A
megvalósítása szintén elég egyszerű, mert tudjuk használni a beépített link generátort (GenerateLink).
Mindössze a route adatokat kell feltölteni az elkódolt értékekkel (GetEncodedString).
public static MvcHtmlString EncodedActionLink(this HtmlHelper htmlHelper, string title, string action,
string controller, int id)
{
var routeValues = new RouteValueDictionary();
routeValues.Add("id", StringEncoderHelper.GetEncodedString(id));
return MvcHtmlString.Create(HtmlHelper.GenerateLink(htmlHelper.ViewContext.RequestContext,
htmlHelper.RouteCollection, title, (string)null, action, controller,
routeValues, null));
}
public static MvcHtmlString EncodedActionLink(this HtmlHelper htmlHelper, string title, string action,
string controller, Dictionary<string, object> routeData)
{
var routeValues = new RouteValueDictionary();
routeValues.Add("id", StringEncoderHelper.GetEncodedString(routeData));
return MvcHtmlString.Create(HtmlHelper.GenerateLink(htmlHelper.ViewContext.RequestContext,
htmlHelper.RouteCollection, title, (string)null, action, controller,
routeValues, null));
}
Az egész működésben megint van megint egy trükk, ami már itt is kirajzolódik, hogy a titkosított értéket
(vagy értékeket a 2. helperben) az „id” nevű route értékben tároljuk. Ennek eredménye lesz egy ehhez
hasonló generált URL:
http://localhost:18005/Security/EncodedUrl/UPosMCqRhMvdI6GzzB61dOC2yJbsXG5K1UMB26I
Mivel az „id” szerepel a default route bejegyzésben így a /-jel után kerül, legalább nem query string
kinézetű az URL vége. Ezzel megvagyunk a generálási oldallal, jöjjenek a titkosított URL-t fogadó
megvalósítások. Megint egy frontvonalbeli IAuthorizationFilter-t megvalósítva, a filterContext-ben
44
A kereső motorok büntetik, ha azonos beltartalom több URL-en is elérhető az adott domainen belül.
9.4 A biztonság és az értelmes adatok - Kódolt azonosítók 1-271
levő RouteData objektumot vesszük kezelésbe. A futásnak ebben a pillanatában, mikor sor kerül az
OnAuthorization használatára, az eredeti RouteData még az elkódolt adatot tartalmazza az „Id” indexű
elemében. Ezt vissza kell sorosítani45. Ennek eredménye két forma lehet a Html helperek miatt: int és
akkor az Id-t jelenti, vagy egy felsorolás és akkor több minden volt az URL-ben (id + query string). Ha
az „id” volt csak egymagában, akkor a RouteData-ba ezzel a névvel kell visszahelyezni a dekódolás után.
Ha felsorolás volt, akkor az egy Dictionaryt rejt és ennek dekódolt bejegyzéseit kell átmásolni a
RouteData-ba. Ha az id-ben levő kóddal bármilyen gond adódna, például, hogy nincs is Id, akkor az
InvalidOperationException megszakítja a futást.
<h3>Elkódolt Id az URL-ben</h3>
@Html.EncodedActionLink("Új oldal, ahova az Id elkódolva érkezik",
"EncodedUrl","Security", Model.HId)
[EncryptedRouteAttribute]
public ActionResult EncodedUrl(int Id)
{
return Content("Id: " + Id);
}
[EncryptedRouteAttribute]
public ActionResult EncodedUrlQuery(int Id, string mokusnev)
{
return Content("Id: " + Id + " Név: " + mokusnev);
}
Bár ezek a kódolt azonosítókkal kapcsolatos példák igen specifikusnak tűnhetnek, viszont jó gyakorlat
volt az eddigi témákra. Nézzük, melyek azok a főbb MVC framework jellegzetességek, amiken
átrágtuk magunkat az előző a példákba bújtatva:
Megnéztük, hogy az AntiForgery rendszert hogyan tudjuk bővíteni és ennek kódjában egy saját
funkcionalitást is rá tudtunk terhelni.
45
vagy: desorosítani, a dekódolni analógiájára.
9.4 A biztonság és az értelmes adatok - Kódolt azonosítók 1-272
Fontos hangsúlyozni, hogy egy saját magunk által implementált security infrastruktúrát nem tanácsos
félvállról, csípőből összedobni. A célunk az, hogy egy biztonsági képességet vigyünk az alkalmazásba,
ami adatot véd. Könnyen eshetünk abba az illúzióba, hogy az adatokat védettnek gondoljuk és nem is
teszünk más óvintézkedéseket, amiket egyébként megtennénk, mert feleslegesnek érezzük. Ráadásul
mások is bízni fognak a megvalósításban a közös fejlesztés során. Nagyon alaposan nézzük át az ilyen
implementációkat, tanácskozzunk másokkal és agyaljunk, hogyan tudnánk kijátszani a saját kódunkat.
Példaként említenék egy 2010-ben felfedezett biztonsági rést az ASP.NET titkosítási rendszerében46.
Évek óta használt megoldásról volt szó, mígnem valaki rájött, hogy az exceptionök típusa és a
válaszidők kombinációjára alapozva vissza lehet fejteni a titkosított értéket. Ezzel az előző példákban
is használt kódblokkokhoz hasonló titkosított adatokat is, mint például az autentikációs cookie-kat is
vissza lehetett fejteni. Említhetek saját friss példát is. Miközben ezt a fejezetet írtam a példakódban
rossz irányban indultam el és RouteValueProviderFactory-n keresztül szerettem volna bemutatni az
URL dekódolást, majd a próbálgatás során rájöttem, hogy simán kijátszható, ha ismerem a query string
neveket. Ezért ezt el kellett vetni. Ezen kívül a fenti, megvalósult példakódokkal is fenntartásaim
vannak, ha nagyon szigorú szemekkel nézek rájuk. Felmerült, hogy az oldalgenerálás során
véletlenszerűen gyártott hidden mezőkbe kerülő kódok vajon elég biztonságosak-e? Nem lehet-e
statisztikai módszerekkel rájönni a timestamp jelenlétére, szabályos időközönként lekérve az oldalt?
Nem kéne esetleg ezt egy kicsit randomizálni +- néhány száz másodperccel eltolva az értékét, ami nem
zavarja meg számottevően a lejárai időt? Milyen következtetéseket lehet levonni abból, hogy a
timestamp visszaalakításakor két eltérő és igen beszédes exception jöhet létre? Ezek fejlesztéskor még
jól jönnek, de éles üzemben ez túl informatív egy security kód belső működéséről.
Érdemes tehát az ilyen kódokkal nagyon észnél lenni, mind az implementációban, mind a
felhasználásában.
46
http://balassygyorgy.wordpress.com/2010/09/18/kritikus-0day-asp-net-sebezhetoseg-es-gyors-vedekezes/
9.5 A biztonság és az értelmes adatok - Validálás 1-273
9.5. Validálás
Az első védelmi vonalról áttérhetünk a következő logikai rétegre, amikor az elemi adatok
érvényességével, tartalmával, értékhatáraival, meglétével tudunk foglalkozni. Mielőtt belevágnánk a
részletekbe, néhány gondolat erejéig tekintsük át a szempontokat, amikre érdemes figyelni, mert
komoly tétje van annak, hogyan fogadjuk a felhasználótól, böngészőből érkező adatokat. Legalább
három olyan fő területet tudnék felsorolni, amiben szerepet kap az érvényes adat.
A program minőségi megítélése. Nem mindegy, hogy egy frissen bevezetett, eladott web
alkalmazást hogyan fogadnak a felhasználók. Képzeljük el, hogy túl vagyunk a rendszer
bemutatásán, látták a vezetők, a megrendelő képviselője, és mindenféle érdekhordozó. A
legtöbb alkalmazást, valljuk be, nem a vezetők használják, hanem a beosztottjaik, akik ráadásul
majdnem biztos, hogy nem fogják elolvasni a felhasználói kézikönyvet, ők ad-hoc akarják
használni a programot. Ezek statisztikai tények, erre fel kell készülni a program fejlesztésénél.
Mi alapján fogja Beosztott Marcsi úgy jellemezni a termékünket a főnőkének, hogy „ez az új
alkalmazás annyira jó, könnyű használni és minden világos”? A vezetőség (már ha értik a
dolgukat) fogadja ezeket a visszajelzéseket és tudva - tudatlanul ott lesz a következő projekt
megbeszélésen, értékelésen. Lehet, hogy az üzleti számítások hibátlanok, az alkalmazás
válaszideje rendben van, a felületet pszichológus közreműködésével tervezték a legjobb
dizájnerrel közösen és még is a felhasználó picit „elveszettnek” érzi magát, amikor használni
kezdi a programot. Ha nem érti meg azonnal, hogy miért „nem megy” a programunk, mikor
egy dátum mezőben a „2013/szept. 4”-t ad meg, akkor nagyon frusztrált lesz. A lényeg, hogy
az alkalmazás használhatóságáról vagy használhatatlanságáról alkotott szubjektív vélemény
mögött ott vannak a hibaüzenetek, a hibás felhasználó interakciójára adott válaszok megléte,
milyensége, szépsége, értelmessége.
Üzleti adatok sértetlensége. Ez általában világos szokott lenni, hisz ezeket az alkalmazás
tervezésekor lefektetik. Sok esetben SQL szinten kényszerek védik (foreign key, not null,
trigger) vagy a szolgáltatások durva exceptionnel reagálnak, ha sérülne a koncepció. Azonban
nem ajánlott ilyen mélységéig elengedni a hibás értékeket. Az architektúrában minél
mélyebben kapunk el egy hibát, annál kisebb a valószínűsége, hogy össze tudjuk egyeztetni a
felhasználói felülettel (vagy nagyon jó tervezők vagyunk). Nehezen visszavezethető, például ha
egy textbox kitöltetlensége miatt a hiba egy tranzakción belüli adatfrissítés közben következik
be. A probléma másik oldala lehet, ha az adat önmagában érvényes, de más adatokkal
összevetve felborítja a rendszerünket. (pl.: 0,76 db kutya)
A böngészőben, javascripttel.
A request beérkezésekor, mielőtt az action metódusunk megkapná. Ezt az MVC keretrendszer
aránylag jól meg tudja oldani és láttunk sok példát arra, ha be szeretnénk avatkozni.
A kontroller actionben, amikor kódból összefüggéseket tudunk vizsgálni.
Az üzleti vagy szolgáltatás rétegben.
Tárolás során. Ez lehet az ORM beépített képessége vagy SQL megszorítások.
A következőkben csak az első három szinttel foglalkozunk, mert a többi nem része az MVC-nek. A
modellek felépítésénél (4.3.2 fejezet) szinte csak felsorolás szerűen végigmentünk a DataAnnotations
attribútumain, de most már az MVC adta lehetőségeket megismerve, a framework egészére jó
rálátásunk van ahhoz, hogy a validálás részleteivel alaposabban tudjunk foglalkozni.
Szemléltetésképpen tegyük fel, hogy a modellen definiálva van egy email mező és ebben az érték.
Mondjuk osztalyvezeto@cegnev.com, akkor ez az 1. kategória szerint érvényes az email cím. Van
benne ’@’ és nem számmal kezdődik, a @ után van két szó ponttal elválasztva, stb. Viszont, ha az üzleti
igény azt fogalmazza meg, hogy a rendszerben nem lehet két egyforma email címmel ilyen entitás,
akkor az email cím mégsem valid, mert más entitásokkal összevetve nem egyedi. A célszerűség,
erőforrás, sávszélesség-takarékosság miatt az 1. kategóriában levő validációt megpróbálhatjuk az
adatbevitel helyéhez minél közelebb vinni, webes rendszereknél például a böngészőben futó javascript
kódba. Az esetek jelentős részében az üzleti logikát propertynként meg lehet oldani néhány sorral, és
emiatt inkább az adatmodell szinten tudjuk implementálni. Mivel ismétlődő kódokról lenne szó, ezeket
attribútumokkal fogalmazhatjuk meg. A 2. kategóriásakat pedig hagyományosan az üzleti logika többi
kódjához közel, kontrollerbe vagy mondjuk a szolgáltatás kódjába érdemes tenni. A mai rendszereknél
gyakori, hogy az olyan helyzeteket, ahol nem lehet két egyforma email cím és/vagy felhasználói név, a
kliensen futó javascript a szervernél, a háttérben kezdeményezi a validációt. Mire átlépne a felhasználó
a következő beviteli mezőre (ahol a jelszót adhatná meg), a háttérben már le lett zongorázva a
szerverrel, hogy a megadott email cím (vagy felhasználói név) elfogadható és egyedi-e. Ekkor a kliens
és a szerver közösen végzi el az elővalidációt.
47
Emlékeztetőnek: ezek a modellpropertyre vonatkozó attribútumok alapján vannak beállítva.
9.5 A biztonság és az értelmes adatok - Validálás 1-275
validációs hiba. Ez így szép és jó, néha azonban az élet kivételeket produkál, olyanokat amikor a saját
magunk által lefektetett és a modellre varrt szabályrendszert rugalmasabban kell értelmezni. (értsd:
áthágni)
A ModelState
Előfordulhat, hogy a modellben definiált validációs attribútumok jók, de egyes actionökben még sincs
szükség bizonyos mezők validációjára. A ModelState nem köti meg a kezünket, mert simán
felülbírálhatjuk a benne megjelent validációs értékeket. Ki is törölhetünk belőle, sőt hozzá is adhatunk.
Ez utóbbira, általában akkor van szükség, ha az attribútum alapú validáció nem elég és bonyolultabb
összefüggések mentén történt validációt kell egyedileg (actionönként eltérőt) meghatározni.
A szokás szerint egy modell készült a vizsgálódáshoz, ami nagyon hasonló a validációs attribútumoknál
látottakhoz. A példakódok a ValidationsController felügyelete alatt vannak.
[Required]
public int RequiredInt { get; set; }
[Required]
public bool RequiredBool { get; set; }
Első nekifutásra most csak a vastagon szedett sorok lesznek fontosak a három Required attribútummal
ellátott propertykkel. A View csak a FullName és az Address propertykre szolgáltat input mezőt:
<br />
@Html.LabelFor(m => m.Address)<br />
@Html.TextAreaFor(m => m.Address, 4, 20, null)<br />
@Html.ValidationMessageFor(m=>m.Address)
<br />
<input type="submit" />
}
Az actionök kódja:
[HttpGet]
public ActionResult ModelStateTest(int? id)
{
return View(ValidationMaxModel.GetModell(id ?? 1));
}
[HttpPost]
[ActionName("ModelStateTest")]
public ActionResult ModelStateTestPost(int? id, ValidationMaxModel inputModel)
{
bool isValid;
if (!id.HasValue) return RedirectToAction("Index");
this.ModelState.Clear();
if (this.TryUpdateModel(model))
{
return RedirectToAction("ModelStateTest");
}
return View(model);
}
Az action futása során a paraméterébe érkező modell (inputModel) miatt a validáció le fog zajlani,
mielőtt az actionhöz érne a vezérlés. Az első isValid azonban true lesz. De vajon miért nem false?
Ha ránézünk a ModelState
tartalmára látható, hogy
mindössze az a három érték
látható, amiknek volt input
mezője a formon is.
A "LastPurchaseDate" és a
"RequiredInt" és "RequiredBool"
nincs sehol.
9.5 A biztonság és az értelmes adatok - Validálás 1-277
A ModelState szintén egy szótár, aminek az értékei (Values) ModelState-ek a kulcsai pedig az input
mezők nevei. Nem keverendő:
A ModelState típusnak létezik az Errors gyűjteménye, ami a validációs hibákat tartalmazza. Annyit,
ahány validációs hiba történt az adott propertyvel kapcsolatban. A képen éppen Count=0 látszik. A
Value propertyje pedig a model binder által igénybevett ValueProvider kimeneti eredménye. Ez az
eredeti érték mielőtt a modellbe került.
Újrafuttatva az actiont, az
isValid már false lesz és a
validációs hiba megjelenik
a ModelState-ben a 3-as
indexű elemnél. Keys[3] és
Values[3] –nál.
A kitöltetlen LastPurchaseDate értéke most már számít, az Errors gyűjteményben a 0. elemnél ott a
hibaüzenet az ErrorMessage tulajdonságban. A gyűjtemény elemeinek száma egy.
9.5 A biztonság és az értelmes adatok - Validálás 1-278
this.ModelState.Clear();
//If-es szerkezet:
if (this.TryValidateModel(inputModel))
{
//Igen mostmár valid.
isValid = this.ModelState.IsValid; // = true
}
A ModelState-et nem csak törölni lehet, akár elemenként is, a már látott példa szerint:
this.ModelState["WillNeverValid"].Errors.Clear();
hanem lehetőség van hozzá is adni az AddModelError metódussal. Ezzel tudunk egyszerűen validációs
hibaüzenetet visszaküldeni, olyan esetben, amikor a validációs probléma az action metódusban vagy
egy WCF szolgáltatásban keletkezett.
A sorban látható, hogy az AddModelError első paraméterével meghatározható, hogy a modell melyik
propertyjéhez kapcsolódik a hiba. Ezt az esetet a @Html.ValidationMessageFor(m=>m.FullName)
jeleníti meg.
A második verzióban nem került megadásra property név, így ezt közös hibaként lehet értelmezni.
Ezt pedig a @Html.ValidationSummary() jeleníti meg, amit rendszerint a beviteli mezők felett vagy
alatt szoktak elhelyezni. Ez tényleg egy összesítő, mert alapértelmezetten minden hibaüzenetet
megjelenít. Emiatt, ha ezt így használjuk, a propertykre helyezett üzenetek megfogalmazásánál
érdemes azt is belevenni, hogy melyik propertyre vonatkozik a hiba. (Erre láttunk már példát, amikor
az ErrorMessage-be írható {0} helyőrző a property nevét vagy display nevét helyettesíti be). A
ValidationSummary(true) csak azokat az üzeneteket fogja megjeleníteni, amelyik nem kapcsolódik
propertyhez, azaz aminek az fenti példában a property neve egy üres string (null nem lehet). Sőt még
kicsit szépíteni is lehet, mert egy további paraméterrel egy fejlécszöveget is meg tudunk jeleníteni.
9.5 A biztonság és az értelmes adatok - Validálás 1-279
Egyedi validátorok
Ez a felsorolás a validációs szabályok kiértékelési sorrendje is. De úgy kell értelmezni, hogy a propertyn
elhelyezett validációs attribútumok előbb értékelődnek ki és csak utána a modell szintű attribútumok.
Ha bármelyik szinten elbukik a validáció, a további vizsgálódásokra már nem kerül sor. Az
IValidatableObject mindig a sor végén lesz, mert csak osztálynak tudunk ilyen definíciót adni. Utolsóból
lesznek az elsők alapján, kezdjük is el ezzel.
IValidatableObject
A példa action metódusa csak annyit tesz, hogy a validációt elindítja, és az eredménytől függetlenül
visszaadja az aktuális oldalt. Így majd megjelenik a validációs üzenet, ha van hiba, illetve lehet új
dátumokkal kísérletezni, ha nincs validációs hiba. A View gyakorlatilag azonos a további példákban.
Csak a modellekkel fogunk variálni.
9.5 A biztonság és az értelmes adatok - Validálás 1-280
[HttpPost]
[ActionName("IValidatableObjectTest")]
public ActionResult IValidatableObjectTestPost(int? id)
{
if (!id.HasValue) return RedirectToAction("Index");
var model = ValidationMaxIVOModel.GetModell(id.Value);
this.TryUpdateModel(model);
return View(model);
}
A metódus visszatérési értéke egy felsorolás, ami a fellépő validációs hibákat tartalmazhatja. Akár
többet is, ha történetesen több propertyvel is problémák adódnának. Emiatt az osztályszinten használt
CustomValidation attribútumnál már előrébb vagyunk. A másik nagy előny ahhoz és a többi validációs
attribútumhoz képest, hogy a modell adataira belsőleg hivatkozhatunk: nem kell felfedni a privát
adatokat, ráadásul az összes tulajdonságot, osztályváltozót is típusosan érhetünk el. Nincs szükség
object->konkrét típus konverzióra, castolásra, dobozolásra.
Teljesen ránk van bízva, hogy hogyan értékeljük ki a szabályokat. A fenti példában az a szabály, hogy a
dátumnak a megadott értékhatáron belül kell lennie.
Ellenkező esetben a visszatérési listába bekerül egy
ValidationResult a hibaüzenetével és a hibásnak talált
property(k) nevével. És itt már tényleg működik ez is.
Csak a szemléltetés miatt, a FullName property alatt is
megjelenik a hibaüzenet. Ennek a validációs
megközelítésnek a hátránya, hogy nincsen referenciánk a Validate metóduson belül a HTTP request
semmilyen adatára, tehát olyan különleges validációt nem tudunk csinálni, amiben speciális, mondjuk
9.5 A biztonság és az értelmes adatok - Validálás 1-281
relatív dátumot kéne érvényesíteni, mint például ”holnap”, ”+3 nap”, stb. Itt már csak olyan adatunk
van, amit a model binder már konvertált a ValueProvidereinek a segítségével.
return badresults;
}
return results;
}
public static new ValidationMaxIVOModel GetModell(int id) {…}
}
A megvalósítás hátránya, hogy csak körülményesen tudjuk meghatározni a validált property nevét a
visszatérési listában. Az oka, hogy a Validator.TryValidateValue a ValidationResult elemű listát belsőleg
tölti ki (results paraméter). Emellett nincs lehetőség a ValidationResult.MemberNames tulajdonság
feltöltésére, mert nincs settere. Így nem marad más hátra, minthogy újra kell csomagolni egy új
ValidationResult listába (badresults).
„Kettőt fizet hármat kap” konstrukcióban álljon itt még egy validációs trükk arra a helyzetre, hogy mi
van akkor, ha a leszármazott osztályon szeretnék az ősosztályra utólag attribútumot definiálni.
Valójában nincs ebben semmi új, mert már láttuk a MetadataType attribútum működését a modell
attribútumoknál a 4.4 fejezetben. Az érdekesség csak annyi, hogy ezzel ezt is meg lehet csinálni és
hasonló lesz az eredménye, mint az előző példának.
[MetadataType(typeof(ValidationMaxIVOModelMetaData))]
public class ValidationMaxIVOModel : ValidationMaxModel { . . . }
A lehetséges válaszok:
a. Kizárólag az alaposztályon levő Range fog csak működésbe lépni, mert mégis csak itt van
definiálva a property.
b. A leszármazotthoz kapcsolt „buddy class”-on a Range attribútum kizárólagosan fog validálni,
mert a MetaDataType attribútum által előírt osztály mindig előrébb van a kiértékelési
logikában.
c. Mindkettő működésbe lép, de a szűkebb dátumintervallummal rendelkező meghatározáson
fog elbukni a 2011-es dátum. Jelen esetben a …MetaData-s osztályon definiált Range
beállításán.
d. Ugyan lefordítható a kód, de nem fog működni a validáció, mert nem lehet két Range egy
propertyhez rendelve. Az eredmény az lesz, hogy exceptiont fogunk kapni.
Nem is olyan egyszerű így a helyes választ megtalálni, főleg ha megindokolom a hibásakat is. Még egy
5 körös állásinterjún is elmenne. A helyes megfejtők között ennek a könyvnek a letöltési linkjét
sorsolom ki... A viccet félretéve, a b. válasz a megfelelő, tehát a MetaDataType osztályon levő
attribútumok mindent visznek, és kizárólagosságot élveznek. Ennek az a nagyszerű következménye,
hogy tudunk olyan leszármazott modellosztályokat definiálni, amivel az alaposztály attribútumait felül
tudjuk bírálni. Mellesleg a d. pont akkor lenne igaz, ha egy propertyre szeretnénk közvetlenül két
Range-et helyezni, de így még lefordítani sem lehet a kódot.
9.5 A biztonság és az értelmes adatok - Validálás 1-283
ValidationAttribute
Szerintem át is ugorhatjuk azt a részt, hogy a meglevő validációs attribútumokból származtatva hozunk
létre egyedi validációt, mert semmi olyan extrát nem adna, amit a ValidationAttribute-ból közvetlenül
származtatott saját megvalósítással ne tudnánk kipróbálni. Tehát induljunk tiszta lappal.
[AttributeUsage(AttributeTargets.Property)]
public class RelativeDateValidatorAttribute : ValidationAttribute
{
public enum RelativeDate
{
ElozoHonap, Ma, KovetkezoHonap
}
Az eddigiek alapján sok magyarázatra nem szorul a kód. Validációs hiba esetén ugyanúgy kitöltött
9.5 A biztonság és az értelmes adatok - Validálás 1-284
ValidationResult-al kell visszatérni. Az AttributeUsage-nek sincs semmi MVC specifikus jellege, alap
.Net fordítási metainformációja. Jelen esetben kiköti, hogy az új attribútumunkat kizárólag propertyn
lehet használni. Másként le sem fordul a kód. Érdemes róla tudni és használni is, ha olyan attribútumot
írunk, aminek esetleg semmi értelme modell szintű validációnál. Nehogy egy kolléga másként akarja
felhasználni. Ezen felül még ezekre hívnám fel a figyelmet:
Ez egy módszer arra, hogy ne kelljen származtatni a meglévő validációs attribútumokból, mégis fel
tudjuk használni azoknak a belsőleg definiált szabályát. Jelen esetben a Required attribútumot
használjuk normál osztályként.
A Providers listát úgy használja az MVC, hogy a …ValidatorProvider-ektől elkéri az általuk ismert és
kezelt ModelValidator-ok listáját. A visszaadott lista az aktuális propertyre vagy modellosztályra
vonatkozik. A TryValidateModel, a TryUpdateModell, a model binder is ezt a listát használja fel a
modell teljes validálására. Az eddigi példákban a DataAnnotationsModelValidatorProvider képességeit
használtuk ki. Ez szolgáltatja a ModelValidator-okat az IValidatableObject és a ValidationAttribute
leszármazottai alapján.
Két módon is közölhetjük az ilyen modellosztályokból, hogy validációs hiba történt. Az egyik, hogy az
9.5 A biztonság és az értelmes adatok - Validálás 1-285
Error propertybe megadunk egy szöveget. Ezt úgy értelmezi, hogy az egész modellel van komplex
validációs probléma. A belső indexere (this[]) a propertykhez rendelt validációs hibaüzenetet
tárolhatja. A columnName property neveket jelent, a megnevezés egy régi örökség. Ez a lehetőség
nagyon hasonlít az IValidatableObject működésére.
Ez a típus alapú validáció egyes esetekben zavaró is lehet. Ilyenkor a global.asax-ban egyszerűen ki kell
törölni ezt a ClientDataTypeModelValidatorProvider-t a statikus listából.
Összefoglalva: a szerver oldali modell szintű validációt, számos helyen tudjuk bővíteni az MVC-ben.
Attól függően, hogy a validációs szabály lebontható-e általános, elemi, érték szintű validációra - amit
szétszórva a modellek között újra tudunk hasznosítani - készíthetünk property szintű attribútumokat.
Több lehetőségünk is van arra, ha a modell belső, összefüggő állapota szerint kell érvényesíteni a
modellre vonatkozó üzleti szabályokat. A validációs megoldásoknak egy közös jellemzőjük, hogy a
fellépő hibát az action számára generálják, és itt megvan a döntési lehetőségünk arra, hogy mit
kezdünk velük. Legtöbb esetben azt szoktuk tenni, hogy a hibát tartalmazó HTML formot
újrageneráljuk és a hibaüzenetekkel együtt visszadobjuk a böngészőnek. Egy egész formot
újragenerálunk és esetleg több száz kilóbájtot küldözgetünk egy darab hibásan kitöltött mező miatt,
mialatt a felhasználó számolja a másodperceket? Valljuk be ez nem szép dolog a mobil internet és sok
csigalassú mobileszköz világában. Nézzük, meg mit tehetünk az ügy érdekében …
@section Scripts {
@Scripts.Render("~/bundles/jqueryval")
}
A fenti Scripts.Render hozzá fogja adni a jQuery validációs függvényeit az oldalhoz. Részletek a 10.3 fejezetben
Mikor az új projektet elkészítetjük a Visual Studio-val az Internet Web Application template alapján,
alapértelmezetten be is van kapcsolva a kliens oldali validáció. Minden View template generáló
dialógusablakban a „Reference script libraries” checkboxot bejelölve a sablon bele is fűzi a fenti
48
A kliens oldali validációt nem szabad készpénznek venni. ld. a fejezet zárszavát
9.5 A biztonság és az értelmes adatok - Validálás 1-286
kódsort az elkészülő View-ba. Ennek legtöbbször csak az Edit célzatú View fájlban van szerepe, mivel
ez foglalkozik leginkább a formok kezelésével. Amúgy, ha az oldalaink jelentős részén használjuk ezeket
a jQuery pluginonkat, célszerű a _Layout.chhtml-ben belinkelni és nem pedig minden View-ba
egyesével belerakni.
9.5 A biztonság és az értelmes adatok - Validálás 1-287
Láttuk már az AJAX formokkal foglalkozó részben a web.config-nak azt a szakaszát, ami a kliens oldali
validációt engedélyezi vagy tiltja az egész alkalmazásra nézve:
<appSettings>
<add key="ClientValidationEnabled" value="true"/>
<add key="UnobtrusiveJavaScriptEnabled" value="true"/>
</appSettings>
Azt is néztük már, hogyha a fenti két beállítás értéke 'true' (és
linkelve vannak a jQuery pluginok), akkor minden további
nélkül beindul a kliensoldali validáció. Még a Save gombot sem
kell nyomni, ahogy elgépelem néhány validátor által kezelt
mező értékét, azonnal jelzi a hibát.
Adott a lehetőség a kliens oldali validáció bekapcsolásához View szinten is. Ha a web.config-ban
kikapcsoljuk, a View-ban még külön engedélyezni lehet:
@{
Html.EnableClientValidation(true);
Html.EnableUnobtrusiveJavaScript(true);
}
A validációs működés hátterének a megértéséhez a legjobb út, ha megnézzük, egy kliens oldali
validátor elkészítésének a lépéseit. Ez a téma szerintem különösen fontos és összetett is, ezért ezt
aprólékosabban nézzük át. A cél az, hogy az előzőleg használt szerver oldali
RelativeDateValidatorAttribute továbbfejlesztésével, az „Utazási nap” (TravelDate) dátumról már a
böngészőben eldőljön, hogy megfelelő-e.
Az MVC keretrendszert úgy tudjuk tájékoztatni, hogy szeretnénk böngésző oldalon is validálni, hogy az
validációs attribútumunk megvalósítja az IClientValidatable interfészt. Ennek az egyetlen előírt
metódusában meghatározhatjuk azt a szabálykészletet, ami a kliens oldali validációt paraméterezi:
public IEnumerable<ModelClientValidationRule>
GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
yield return new RelativeDateClientValidationRule(rdate);
}
A yield return ebben az esetben egy egyelemű enumárációt fog eredményezni.
A szabályt hordozó osztály egy ModelClientValidationRule leszármazott lehet. Ezt valósítja meg a
RelativeDateClientValidationRule osztály. A lenti kód nagyobbik része a kódba varrt enum-függő
hibaüzenet, ami majdnem egy kódismétlés az ősosztályból. Csak azért ilyen hogy egyben lássuk, és
9.5 A biztonság és az értelmes adatok - Validálás 1-288
hogy elkülönüljön a hibaüzenet szövege a …(kliens)!" végződéssel, a szerver oldali változattól. Valós
életben ezt érdemes amúgy is resource fájlból megoldani. A lényeg a végén található két sor.
ValidationType = "daterelative";
ValidationParameters.Add("reldate", relativeDate.ToString().ToLower());
}
}
A ValidationType egy jQuery validation plugin funkciót határoz meg, amit majd nekünk kell megírni JS
kódban. A ValidationParameters <string, string > szótár elemei pedig bekerülnek a generált HTML input
mezőbe, mint data-* attribútumok. Jelen esetben csak egy elem a „reldate”. A generált HTML
(unobtrusive) attribútumok elnevezési konvenciója ilyen:
<input
data-val="true"
data-val-daterelative="A dátum csak az előző hónapi lehet (kliens)!"
data-val-daterelative-reldate="elozohonap"
id="TravelDate" name="TravelDate" type="text"
value="2013.06.06. 0:00:00" class="valid"
/>
<input
data-val="true"
data-val-required="A név megadása kötelező (1)!"
id="FullName" name="FullName" type="text"
value="Tanuló 1" class="valid"
/>
Visszakövetkeztetve ez annyit jelent, hogy léteznie kell egy hozzá tartozó Rule-nak is. Ez így néz ki
eredetiben:
Ránézésre egy ilyen ModelClientValidationRule megvalósítás semmit sem csinál a konkrét osztályon
belül, még egy propertyje sincs és csak az ősosztály propertyjeit töltögeti. Az egyik nyomós ok, hogy
miért csinálunk minden egyes rule-hoz egy új osztályt az, hogy a ValidationType által meghatározott
jQuery plugin-funkció regisztrálásra kerül. Kettő azonos nevűt nem lehet beregisztrálni a jQuery
validation-ba. Így minden JS plugin funkcióhoz tartozik egy C# ModelClientValidationRule-ből
származott osztály. Így rend van. Egy másik szabály, hogy az attribútummá váló paraméterek és nevek
legyenek kisbetűsek, mert ez meg HTML ajánlás. (daterelative, reldate, required, stb)
Ehhez két funkciót kell implementálni. Az egyik egy „adapter” a másik a konkrét validációt megvalósító
kód. Az adapter képezi a kapcsolatot az unobtrusive formában létrejött HTML markup és a jQuery
validátor között. Ez szolgáltatja a bemeneti értéket és a hibaüzenetet. Egy fontos kitétel, hogy ezeknek
a kódoknak a jQuery validation JS fájlok linkelése után kell lenniük. Akárhova is tesszük ez a sorrend
fontos.
@section Scripts {
@Scripts.Render("~/bundles/jqueryval")
}
<script type="text/javascript">
//Adapter:
$.validator.unobtrusive.adapters.addSingleVal("daterelative", "reldate");
//Validátor:
$.validator.addMethod("daterelative", function (value, element, relativeDate) {
return false;
});
</script>
Azonban ez így nem jó! Nem biztos, hogy működni fog ez a sorrend. Látszólag a jqueryval script csomag
után következik a mi <script> blokkunk. Emlékezzünk vissza a @section és a @rendersection razor
funkciók működésére. A sorrendet nem a @section, hanem a @rendersection fogja meghatározni a
layout.cshtml-ben. Egy MVC sablonnal készített projekt esetén az oldal végére fogja beszúrni a
jqueryval által meghatározott JS könyvtárakat. Tehát a mi kódunk után fog következni.
Így lesz jó, mert a section blokkon belül biztos, hogy a jQuery linkelések után fog megjelenni a mi
kódunk a HTML markupban:
@section Scripts {
@Scripts.Render("~/bundles/jqueryval")
<script type="text/javascript">
//Adapter:
$.validator.unobtrusive.adapters.addSingleVal("daterelative", "reldate");
//Validátor:
$.validator.addMethod("daterelative", function (value, element, relativeDate) {
return true;
});
</script>
}
9.5 A biztonság és az értelmes adatok - Validálás 1-290
Az adapterek közé nem csak ilyen egyparaméteres validációs szabályt lehet felvenni, hanem van még
három másik szignatúra is:
Térjünk át most a validátorokra. A formája elég egyszerű. A "value" funkcióparaméter azt az értéket
hordozza, ami az input mezőből jön szövegesen. Az "element" maga az input mezőre hivatkozó jQuery
objektum (az amit a jQuery szelektor visszaadott). A lenti példában a "relativeDate" a paraméter
értékét adja. Arról a paraméterről van szó, amit az addSingleVal híváskor meghatároztunk. Az érték
pedig a HTML input mező data-* attribútumából jön szöveges formában. Emiatt pedig a kliens oldali
validáció átverhető, ha ezt a HTML attribútumot azelőtt átírom, mielőtt az adapter regisztráció lefutna!
<script type="text/javascript">
//Adapter:
$.validator.unobtrusive.adapters.addSingleVal("daterelative", "reldate");
//Validátor:
$.validator.addMethod("daterelative", function (value, element, relativeDate) {
var currentDate = new Date();
var inputDate = new Date(value);
switch (relativeDate) {
case "@RelativeDateValidatorAttribute.RelativeDate.ElozoHonap.ToString().ToLowerInvariant()":
var prevMonth = GetRelativeMonthOnly(currentDate, -1);
var inputDatep = GetRelativeMonthOnly(inputDate, 0);
if (+inputDatep != +prevMonth)
return false;
break;
case "@RelativeDateValidatorAttribute.RelativeDate.Ma.ToString().ToLowerInvariant()":
var actualDay = currentDate.setHours(0, 0, 0, 0);
var inputDatec = inputDate.setHours(0, 0, 0, 0);
if (+inputDatec != +actualDay)
return false;
break;
case "@RelativeDateValidatorAttribute.RelativeDate.KovetkezoHonap.ToString()
.ToLowerInvariant()":
var nextMonth = GetRelativeMonthOnly(currentDate, +1);
var inputDaten = GetRelativeMonthOnly(inputDate, 0);
if (+inputDaten != +nextMonth)
return false;
break;
}
return true;
});
A JS dátumkezelő mechanizmusát most nem részletezném túl. Itt is azt a módszert használom, hogy a
hasonlítandó hónap dátumintervallum vizsgálata helyett a hónap napját 1-re állítom. Ami példában az
az MVC szemszögéből gyakorlatiasabb az, hogy a JS kódba a statikus case értékeket a szerver oldalon
szúrom bele. Ahelyett, hogy a
case 'elozohonap':
case 'ma':
case 'kovetkezohonap':
esetek a kódba lennének égetve, inkább az enum érték szöveges megjelenését rendereltem bele. Pl.:
case "@RelativeDateValidatorAttribute.RelativeDate.ElozoHonap.ToString().ToLowerInvariant()":
Ha az enum egyik tagját átnevezném, akkor nem kell a JS kódot is átírni, mert a VS Refactor/Rename
képessége kicseréli mindenhol. Egyébként én biztos elfelejteném, hogy itt is módosítani kéne, de ezzel
más is így van. Ennek az a feltétele, hogy a JS kód egy View-ba legyen ágyazva, mint ahogy a fenti
példában is van. Azonban ez a beágyazás, nem a legjobb módszer akkor, ha az oldalgenerálás
sebességére is figyelemmel akarunk lenni. A JS kódokat érdemes külső .js fájlba tenni. Előnyök-
hátrányok.
9.5 A biztonság és az értelmes adatok - Validálás 1-292
Legalább egy tucat beépített validátor és hozzá tartozó adapter van definiálva, akár ezeket is fel lehet
használni. A működés alapját biztosító jQueryvalidator.js pluginról még számos további érdekességet
lehet találni a honlapján: http://jqueryvalidation.org. Teljesen testre lehet szabni a működését, a
megjelenítéstől az eseménykezelőkön át szinte mindent. Példának csak egyet emelnék ki, ami hasznos
lehet. Ezzel le lehet tiltani, hogy a validáció lefusson minden egyes billentyűnyomás után:
Ezután csak akkor fog validálni, amikor az input mező elveszti a fókuszt, a felhasználó átlép egy másik
mezőre. Még az előtt is validál, hogy a formot submittal visszaküldenénk.
Eddig eljutva, azt is gondolhatnánk, hogy minden rendben van. Ennyi validációs képességgel mindent
meg tudunk oldani. Valójában nem ilyen egyszerű az élet. Ott vannak például azok a helyzetek, amikor
a kliens oldalon szeretnénk azt a szituációt megoldani, hogy a felhasználó ne adhasson meg olyan
értéket, ami már foglalt, aminek egyedinek kell lennie. Email cím, felhasználói név, szobafoglalás adott
időpontra, stb. Számos ilyen helyzet van. Milyen bosszantó, amikor egy kitöltött regisztrációs formot
beküldve a webalkalmazás visszadobja, hogy „sorry, a felhasználói név foglalt”. Nem beszélve arról,
hogyha jelszót is igényelt az űrlap, akkor azt újra be kell gépelni kétszer is. Fú, de útálom az ilyet. Az
ilyen szerver+kliens együttműködésén alapuló ún. kompozit validációra is van támogatás az MVC-ben
a RemoteAttribute használatán keresztül.
49
(Chrome böngészőben a címsorba írva: chrome://settings/. -> Speciális beállítások link-> Tartalom beállítások
gomb. Az ablakban a JavaScript szekcióban lehet letiltani vagy engedni)
9.5 A biztonság és az értelmes adatok - Validálás 1-293
[MetadataType(typeof(ValidationMaxRemoteModelMetaData))]
public class ValidationMaxRemoteModel : ValidationMaxModel
{
public static bool IsNameReserved(string newname)
{
return datalist.Any(d=>d.Value.FullName == newname);
}
A modell - ami megint egy leszármazott - tartalmaz egy kiegészítő metódust, amivel eldönthető, hogy
a FullName-ben tárolt érték foglalt-e már a datalist listában (még mindig ez a tárolónk). Így a
ValidationMaxRemoteModelMetaData osztályban definiált FullName propertyre raktam rá az
attribútumot. Ennek az attribútumnak az első két kötelező paramétere a kontrollert és az actiont
azonosítja. Ezen felül még néhány kiegészítő:
Nézzük az actiont:
[HttpPost]
public JsonResult RemoteNameValidator(string FullName)
{
if (ValidationMaxRemoteModel.IsNameReserved(FullName))
{
//1. változat az attribútumon definiált hibaüzenet (ErrorMessage, stb.)
return Json(false);
//2. változat szerver oldali hibaüzenet direkt módon
return Json("Sajnos a név már foglalt (controller message)");
}
return Json(true);
return Json("true"); //Ez is megfelelő
}
9.5 A biztonság és az értelmes adatok - Validálás 1-294
Az actionhöz a hívás Json formában érkezik és így is kell visszaválaszolni. Célszerű használni a
validálandó attribútum nevét, mint paramétert (string FullName), mert így könnyen átvehető az eddig
begépelt érték a kliensen. Az actionben kiértékeljük a kapott értéket és visszaküldjük a választ. Ennek
a válasznak a tartalma négy féle is lehet.
true, vagy "true" – jelzi, hogy a validáció nem talált hibát. Lehet bool vagy annak szöveges változata is.
false – hibás az érték. Ennek megfelelően a Remote attribútumban beállított hibaüzenet jelenik meg a
felületen. A "false" string nem használható, ld. a következőt:
"Hibaüzenet" – hibás az érték, de ez az üzenet jelenjen meg, és ne az, ami az attribútumhoz kapcsolt
alapértelmezett hibaüzenet. Ezt jelzi a demó üzenetek végén a záradék (controller message) és
(attribute message), így látható lesz, hogy honnan származik az üzenet.
,AdditionalFields = "Address,Id")
Action szignatúra:
Ehhez természetesen az kell, hogy a View-ban benne legyenek, mint input mezők. A példa is sugallja,
ha mondjuk egyedi nevet akarunk, de csak lakóhelyenként, így megoldható. Az Id átvételével például
az adatbázisból (repositoryból, Sessionből, Cache-ből) is elkérhetjük újra az objektumot.
A Remote attribútumos validációnak van két hátránya. Az egyik, egy nagyon alattomos jelenséget okoz.
Arról van szó, hogy egy textbox kitöltése közben gyakorlatilag minden billentyűfelengedés után elindul
egy kliens-szerver validációs ciklus. Vajon mi történik, ha egy pörgős ujjú felhasználó kezd el gépelni,
és a billentyűlenyomások között eltelt idő kisebb, mint a szerver válaszideje? A szerverválasz
megérkezéséig nem indul újabb validáció és a pillanatnyi inputmező tartalom sem kerül be valamilyen
várakozási sorba. Így előállhat olyan szituáció, hogy a felhasználó begépelt egy elfogadható értéket, de
egy megelőző validációs ciklus üzenete jelenik meg, ami arról tájékoztatja, hogy hibás az érték, mert
az addig begépelt szöveg nem volt érvényes. Na most, ha a szerveroldali validációs kód komolyabb
adatbázis műveletet végez (pl. egy teljes táblát relációkkal átnéz, hogy az érték foglalt-e), sokkal
nagyobb az esélye, hogy a felhasználó gyorsabban gépel, mint a szerver válaszideje. Ahogy nő a
lekérdezésbe bevont táblasorok száma úgy növekedhet a válaszidő is, vagy más időbeni lassulás is
lehetséges, ahogy a programot használják. Ennek az lehet az eredménye, hogy a kis elemszámú
tesztadatbázison és az alkalmazás bevezetés kezdeti szakaszában minden jól működik, majd az idő
előrehaladtával alattomosan bújik elő a hiba. Esetleg más helyzetben egy lassú internet kapcsolaton
jön csak elő, fejlesztői környezetben soha.
9.5 A biztonság és az értelmes adatok - Validálás 1-295
Az actionben egy másodperces késleltetést szimulálva könnyen elő tudtam idézni, hogy az egyébként
elfogadható "Tanuló 5" alatt megjelenjen a "Tanulo 4" (foglalt) hibaüzenete.
Szerencsére ez a hiba ritka esetben kerül elő, mert a kliens oldali validáció nem csak akkor fut le, amikor
gépelnek, hanem akkor is, ha átlépnek egy másik input mezőre. Így ha van a beviteli mező alatt egy
másik is, akkor ki fogja javítani a téves üzenetet. Mindenesetre érdemes észben tartani, hogy a
működésből adódóan ilyen inkonzisztens helyzet is előállhat. Nem is tettem volna említést erről a
jelenségről, ha ez a probléma nem mutatna túl a Remote attribútumon. Valójában az összes kliens
oldali interakció szerver oldali feldolgozása esetén előállhat ilyen probléma. Nem korlátozódik csak
ennek az attribútumnak a működésére.
Nem utolsó sorban, a billentyűnyomásra induló validáció itt is letiltható a már látott módon. Csak
akkor azt is bele kell venni a specifikációba, hogy a kompozit validáció csak fókuszváltáskor indul be.
A másik – az előzőnél is nagyobb - hibája a RemoteAttribute-nak, hogy nem végez szerver oldali
validációt. Ez viszont nyers hiányosság. A probléma akkor jelentkezik, ha a böngészőben kikapcsoljuk a
javascript feldolgozást. Nem fog validálni semmi, mert a validációt végző segéd-action nem fog
meghívásra kerülni. Nemes egyszerűséggel az attribútum szerver oldali validációs kódja ennyi:
Ez a hiányosság lehetőséget ad arra is, hogy hamis/manipulált post requestet küldjünk a szerverre
megkerülve a validáció. A Remote attribútum tehát egy félkész megoldás. Mivel az ilyen validációs
helyzetben amúgy is az van, hogy egy speciális ellenőrzőkódot kellett implementálni egy action
metódusban, ezért célszerű felülbírálni Remote ősén definiált másik IsValid metódust. Erre gondolok:
A célt úgy is el lehet érni, ha a leszármaztatott Remote attribútum az action metódust hívja meg a
validációért. Ezt az érdekes (vagy inkább nyakatekert) megközelítést mutatja be egy CodeProject cikk:
Egy kérdéssel zárnám le ezt a témát: bízhatunk-e a böngészőben lefutó validációban? Egyértelműen
nem, mert nem lehetünk benne biztosak, hogy valóban megtörtént-e. Még akkor sem, vagy akkor főleg
nem, ha ezt megelőzően a javascript kód szerver oldali kisegítő validációt igényelt. A kliens oldali
validációnak több köze van a felhasználói élmény javításához és az erőforrás takarékossághoz, mint a
valódi értelemben vett adatellenőrzéshez. Ezért egy kliens oldali validációt mindig követnie kell egy
szerver oldali validációnak is. Szerencsére, mint láttuk a beépített validációs attribútumok tudják ezt
(kivétel a Remote, mert itt nekünk kell tudnunk…).
9.5 Reakcióképesség, gyorsítás, minimalizálás. - Validálás 1-297
Nem éppen vendégmarasztaló az olyan oldal, ami a szerver túlterhelése miatt hiányosan, összetörve
jelenik meg. Gondolom mindenki látott már félig letöltött oldalt, ami 10-30 másodperc alatt jelenik
meg úgy-ahogy, képek és stílus nélkül. Ezek oka leggyakrabban az szokott lenni, hogy a teljes oldalt
összehozó 10-50 további HTTP requestek közül nem mindet sikerül időben kiszolgálnia a túlterhelt
szervernek. Esetleg a hálózati kapacitás kevés arra, hogy összesítetten 1-2 Mbyte adatot átküldjünk a
böngészőnek. Talán az oldal megjelenik jól, de a munkát az oldallal csak 10-30 másodperc után lehet
kezdeni. Ilyenkor a felhasználók első reakciója, hogy frissítik az oldalt, ezzel előáll a "szegényszervert
még az ág is húzza" szituáció, mert újra kierőlködheti magából az egész kiszolgálási ciklust feleslegesen,
és újra leterheljük a hálózat átbocsájtó képességét is. Lehetséges, hogy a felhasználó ismeri a Ctrl+F5
kombót, amivel a böngészőben gyorsítótárazott képeket, szkripteket is újra elkérheti a szervertől, és
jaj az alkalmazásunknak. Ebből a kis bevezetőből már is lehet érzékelni, hogy egy nem megkerülhető
témáról van szó.
oldalt vagy csak annak változó részeit. A statikus(abb) tartalmakat lehet tárolni más
szervereken (CDN), amiknek szintén megvan a szerver-proxy-kliens cache-elés láncoltatási
lehetőségük.
Azonban a cache-elés sem csodaszer. Látható, hogy valahol fizetni kell a kevesebb CPU üzemért, a
kisebb adatcsomagért. Ez a fizetség lehet egy nagy memória, egy nagyon gyors fájlrendszer, sok-sok
kódolási munkaóra, a CDN havi forgalmi költsége vagy mindezek együtt. Az erőforrások közti egyensúly
fenntartása nem kis tervezői feladat, ráadásul fel kell készülni arra, hogy ezt az egyensúlyt bármikor át
tudjuk állítani. Ezért is van ma annyira létjogosultsága a Cloud rendszereknek, ahol ez az egyensúly
webfelületről csúszkákkal szabályozható a költségvetés és az szezonális igény függvényében.
10.1. Az OutputCache
Nézzük a következő szituációt: Látogató Margit számára elérhető oldalt az első generálással együtt a
cache-be is betöltjük. Két perc múlva az oldal újralekérése, már gyorsabb lesz, hiszen a cache-ből fog
előállni. Csakhogy időközben (mondjuk 1 perc 55-nél) Margit jogát megvonták az oldal megtekintésére,
vagy esetleg az oldalt törölték, tartalmát javították, vagy bármit tettek, ami érvénytelenné teszi a
cache-elt tartalmat. Emiatt a cache-elésnek vannak feltételei, függőségei. A legfőbb ilyen függőség az
érvényességi idő, de lehetnek további körülmények, amik lejárati idő előtt érvénytelenné teszik a
tartalmát. Például a jogosultság megvonása Margit esetében.
Az ASP.NET + MVC számos támogatást nyújt erre a feladatra, de egy átgondolatlan beállítás esetén
előfordulhat, hogy a cache-elés áthúzza a számításunkat. Az alap ASP.NET platform által szolgáltatott
rendszert teszi könnyen elérhetővé az OutputCache attribútum. Úgy működik, hogy az action első
futásának a HTML eredményét a Cache-be rakja. Majd egy későbbi új request esetén, amennyiben az
actionhöz tartozó érvényes bejegyzés megtalálható még a Cache-ben, akkor az action kódja nem lesz
lefuttatva, helyette a tárolt tartalom kerül kiküldésre. Az OutputCacheAttribute a paraméterezése
nagyjából lefedi az alap ASP.NET output Cache funkcionalitását.
Duration
Ez a paraméter egy egészmásodperc alapú lejárati időt határoz meg. A 0 érték azt jeleni, hogy nincs
lejárat, és a cache-elés az alkalmazás újraindulásáig tart. Nincs alapértelmezett értéke, amíg a
web.config-ot megfelelően nem állítjuk be, ezért a végtelen lejárati időt is jelezni kell egy Duration=0
paraméterrel. Az alábbi példa 20 másodperces gyorsítótárazást ír elő az action által generált HTML
tartalomra, úgy hogy a cache érvényessége nem függ semmi mástól csak az időtől:
[HttpGet]
[OutputCache(Duration = 20, VaryByParam = "none")]
public ActionResult CacheTest(int? id)
{
return View(CacheDemoModel.GetModell(id ?? 1));
}
10.1 Reakcióképesség, gyorsítás, minimalizálás. - Az OutputCache 1-299
VaryByParam
Az ilyen VaryBy… -al kezdődő paraméterek a cache-elt tartalmat különböző névindexű cache
bejegyzésekbe irányítják. Ezzel különböző, elkülönült eseteket tudunk meghatározni, ami szerint a
cache-elt tartalmat elszeparálhatjuk a VaryBy… által meghatározott szempontok szerint. De csak
óvatosan, mert rosszul átgondolt helyzetben (hosszú érvényességi időnél, nagy oldalméretnél, sok
terméknél) sok memóriát fognak fogyasztani, kis hatékonysággal.
A VaryByParam a Get request esetén az URL-ben levő query string(ek) nevei alapján képzi a cache
bejegyzés nevét. Post request esetén a nevek a post adatokból (input mezők neveiből) képződnek. Ez
azt jelenti ennél a paraméternél, hogy a /CacheDemo/CacheTest/1 oldalt külön fogja cache-elni a
/CacheDemo/CacheTest/2 oldal generált eredményétől. Az "1" és a "2" az Id, mint route paraméter az
OutputCache szempontjából paraméternek számítanak, habár az URL-ből nem látszanak query string-
nek. (Sőt az MVC régebbi verziói sem így kezelték, az MVC3 óta működik így). A VaryByParam
paramétere a requestben szereplő nevek (query string vagy post adat) pontosvesszővel elválasztott
listája vagy csak egy darab név. Amikor több nevet is megadunk, akkor azok minden együttes
kombinációjára elkülönítést fog meghatározni.
A kipróbáláshoz egy olyan View-t készítettem, ami három további child actiont indít, de azok különböző
Id-jű modelleket jelenítenek meg. Az OutputCache ezeknek a child actionöknek az eredményét fogja
tároltatni. A fő/parent action eredményét nem cache-eljük. Ez is egy különleges lehetőség, hogy lehet
oldalgenerálási részleteket is külön gyorsítótárazni ebben a megvalósítási formában.
<div>
Pontos idő : @DateTime.Now.ToString("yyyy.MM.dd HH:mm:ss.fff")
</div>
<tr>
@Html.Action("CacheTestChild1", new { id = 1 })
</tr>
<tr>
@Html.Action("CacheTestChild1", new { id = 2 })
</tr>
<tr>
@Html.Action("CacheTestChild1", new { id = 3 })
</tr>
A modell úgy van elkészítve, hogy a GetModell metódus beállítja a modell lekérdezési időpontját a
SelectTime propertybe, ezzel bizonyítva, hogy mikor fordult a rendszer a GetModell metódushoz. Ami
akkor fog megtörténni, ha az action kódja lefut. Ez pedig csak az első lekéréskor és a meghatározott
cache érvényességi idő lejárta után történik meg.
10.1 Reakcióképesség, gyorsítás, minimalizálás. - Az OutputCache 1-300
Az eredményen látható, hogy a három child action hívása a "Felhasználó név"-ben eltér egymástól, de
a "Lekérdezési időpont" a "Pontos idő"-höz képest régebbi, a hívás pillanatában a modell nem lett
lekérdezve.
Az OutputCache attribútum (MVC 3 óta) igazából nem is igényli a VaryByParam kitöltését, ha az action
paramétert vagy paramétereket vár. Alapértelmezetten a paraméterek neve alapján is beállítja
VaryByParam értékét, ha mi nem töltjük ki. Ebben a példában az 'id' szerint különíti el a cache-elt
tartalmakat.
[OutputCache(Duration = 10)]
public ActionResult CacheTestChild1(int? id)
VaryByHeader
Az előző paramétertől csak annyiban különül el, hogy a HTTP fejlécben levő nevesített adatok alapján
határoz meg elkülönítési szempontot, vagy szempontokat, mert ez is képes pontosvesszővel
elválasztott névlistát fogadni. A felhasználása elég ritka esetben kerül elő, mivel a HTTP header nem
sok olyan információt hordoz, ami miatt érdemes lenne elkülöníteni. A request HTTP header tartalma
általában a következő név-értékpárokat hordozza:
text/html,application/xhtml+xml,application/xml;q=0.9,*
Accept /*;q=0.8
Accept-Encoding gzip, deflate
Accept-Language en-US,en;q=0.5
Cache-Control max-age=0
Connection keep-alive
Host localhost:18005
Mozilla/5.0 (Windows NT 6.2; WOW64; rv:21.0)
User-Agent Gecko/20100101 Firefox/21.0
Az alábbi kódok azt példázzák, hogyan lehet böngésző környezetenként elkülöníteni a cache-t.
[HttpGet]
[OutputCache(Duration = 120, VaryByHeader = "User-Agent")]
public ActionResult CacheTestHeader(int? id)
{
return View(CacheDemoModel.GetModell(id ?? 1));
}
A kipróbálásához legalább két különböző böngészőre van szükség, hogy látható legyen a működés.
Mivel HTTP header-ről van szó nem használhatjuk child actionön, ami nem kap külön requestet, mikor
az Html.Action meghívásra kerül.
Ami érdekes még ebben a példában, az nem is annyira a HTTP header szerinti elkülönítés, hanem az
OutputCache-nek az a tulajdonsága, hogy alkalmazás szintű a működése. Ez azt jeleni, hogy nem
különül el felhasználónként, session-önként, sőt User-Agent-enként sem, ha most nem határoztuk
volna meg.
VaryByContentEncoding
VaryByCustom
Szinte már minden elképzelhető helyzetet el tudunk különíteni, de mégsem elég jól. A VaryByHeader
példánál az a probléma, hogy nem csak böngészőnként különít el, hanem böngészőnként/böngésző
verziónkként/operációs rendszerenként, szóval bármi apró eltérés van az User-Agent-ben az külön
cache bejegyzést jelent. A cache bejegyzések elkülönítésének a speciális módját nyújtja ez a paraméter.
10.1 Reakcióképesség, gyorsítás, minimalizálás. - Az OutputCache 1-302
A paraméter értékében szintén pontosvesszővel elválasztott listát tudunk megadni. Ezek a listaelemek
csak egy egyedi azonosítók, amiket az alkalmazás global.asax kódjában tudunk egyedileg lekezelni.
Ez már közelebb visz a Cache működésének a megértéséhez. A Cache-be kerülő értékek, szöveges
azonosítók szerint vannak indexelve. A fenti metódus szerepe csak annyi, hogy egyedi kulcsot
generáljon a speciális requestek számára. A specializálódást a böngésző fő- és alverziója jelenti, illetve
az, hogy a böngésző mobil eszközön fut vagy nem. Innentől bármit ami a HttpContext-ben elérhető,
felhasználhatunk cache kulcsként. Az alábbi deklaráció használható a GetVaryByCustomString
metódus felülbírálása nélkül is.
Location
A Cache, mint szerveroldali gyorsítótár mellett, elő lehet írni a kapcsolatban résztvevő más szereplők
számára is a gyorsítótárazási viselkedést, az OutputCacheLocation enum értékével.
A létrejött HTML tartalmat képes eltárolni a böngésző mellett, a szerver és a böngésző között lévő
proxy szerver(ek) is. A szerveren futó Cache infrastruktúra nyilván tudja, hogy melyik oldalhoz tartozó,
melyik cache bejegyzés mennyi ideig érvényes. Azonban, hogy a böngésző és a proxy szerver is
értesüljön erről, a reposnse fejlécben kiküldésre kerülnek ezek az irányelvek is. Ezek között az egyik
lényeges érték a fenti táblázat utolsó sorában is felsorolt Cache-Control. Az itt látható táblázat egy 120
másodperces "public" tárolású HTTP response header kivonata:
Cache-Control no-cache
Date Tue, 11 Jun 2013 19:24:37 GMT
Expires -1
Pragma no-cache
10.1 Reakcióképesség, gyorsítás, minimalizálás. - Az OutputCache 1-304
CacheProfile
Ahhoz, hogy ne kelljen számtalan helyen újra és újra megfogalmazni a cache feltételrendszerét, vagy
szeretnénk egy egységes kiinduló alapot adni minden OutputCache számára, nyitva áll a lehetőség,
hogy a cache beállításokat profilokba szervezzük. Ebben az esetben a CacheProfile paraméter szöveges
értéke egy szabadon választott profilnév.
[OutputCache(CacheProfile = "OtPercVaryById")]
public ActionResult CacheTestProfile(int? id)
{
return View("CacheTestHeader", CacheDemoModel.GetModell(id ?? 1));
}
<system.web>
<caching>
<outputCacheSettings>
<outputCacheProfiles>
<add name="OtPercVaryById" duration="300"
varyByParam="Id" />
<add name="HatvanMasodpercVaryByNone" duration="120"
varyByParam="none" />
</outputCacheProfiles>
</outputCacheSettings>
</caching>
</system.web>
<caching>
<outputCache enableOutputCache="false" />
</caching>
SqlDependency
Az ASP.NET alaprendszere lehetőséget ad arra, hogy a cache bejegyzés érvényességét egy SQL
lekérdezéshez kapcsoljuk. Abban az esetben, ha az SQL lekérdezés eredménye eltér az cache bejegyzés
készítésekor fennálló állapottól, a bejegyzést érvénytelennek minősíti. Mivel a legtöbb esetben egy
dinamikus oldal előállítása és a benne megjelenő dinamikus tartalom egy adatbázis lekérdezésből
származik, nyilvánvalóan az előállított és eltárolt cache-elt tartalom értelmét veszti, ha az
előállításához használt adattartalom megváltozik. Sajnos a beállítása nem túl egyszerű és be kell
vallanom, hogy még egyszer sem használtam, ezért nem lenne hiteles, ha itt elkezdeném ezt
részletezni. Viszont Csala Péter egyszer régen elhatározta, hogy ír róla egy három részes cikksorozatot,
ami lépésről lépésre bemutatja a használatát. Ezt tudom ajánlani azoknak, akiknek felkeltette az
érdeklődését: http://csalapeter.wordpress.com/2007/05/21/query-notification-i-resz/
10.1 Reakcióképesség, gyorsítás, minimalizálás. - Az OutputCache 1-305
[OutputCache(Duration = 10)]
public ActionResult CacheTestChild1(int? id)
{
return PartialView("CacheTestChild", CacheDemoModel.GetModell(id ?? 1));
}
Ennek eredménye, hogy a fő oldalon levő pontos idő minden oldallekéréskor frissült, mert nem volt
cache-elve, míg a táblázat Tanuló1-2-3 sorai cache-elődtek.
Vajon mi történik, ha megfordítjuk a lejárati időket és a fő View actionje hosszabb duration értéket
kap, mint a child action? Például így:
[HttpGet]
[OutputCache(Duration = 15)]
public ActionResult CacheTestParentSlow()
{
return View();
}
[OutputCache(Duration = 5)]
public ActionResult CacheTestChildFast()
{
return PartialView("CacheTestChildFast");
}
Az eredmény az lesz, hogy a child action futására addig nem is kerül sor, amíg a parent action
(CacheTestParentSlow) cache ideje le nem jár. Azaz a child action cache beállítása ebben az esetben
felesleges és haszontalan. A szakirodalom vizuális névvel említi a partial cache megvalósítást: Donut
caching. Arról van szó - a lyukas közepű fánk képét használva – hogy az MVC csak arra ad lehetőséget,
hogy a fánk lyukas részét vagy az egész fánkot tudjuk cache-elni. A fánkot a lyuk nélkül nem. Általában
egy web oldalra inkább az a jellemző, hogy a fejléc-lábléc szekció ritkán változik, a beltartalom annál
inkább. Még jó hogy, hogy elérhető egy kiegészítés, amivel a fánk ízesebb részét tudjuk kizárólagosan
cache-elni:
http://mvcdonutcaching.codeplex.com/
Azonban nem kell rögtön fűhöz-fához rohanni, ha valami nem úgy működik, ahogy jó lenne. A Layout
+ View felépítését úgy is meg tudjuk szervezni, hogy a View-t kiszolgáló actiont nem látjuk el cache-
eléssel, de lényegi tartalommal sem. Mindössze további child actionök indítására használjuk, amikre
ráadásul egyedi cache irányelveket tudunk meghatározni.
Igaz, így most szükségünk lehet hat különböző action-re, de a Layout-hoz kötődő Fejléc és Lábléc
actionöket egy közös "Common" kontrollerben csak egyszer kell megvalósítani. A View-ban sem biztos,
hogy kell három szekció (persze lehet több is). Ennek a felépítésnek még egy előnye van, hogy az AJAX-
os, részleges oldalfrissítéshez is jól illeszkedik. Példának okáért a "Középső rész" szekció, a maga 0
cache idejével, teljes egészében AJAX frissítésű lehet.
A fánkos cache-elésnek van egy kikötése, nem mintha sok értelme lenne, de az OutputCache-t és az
Authorize attribútumot nem használhatjuk egy child actionön egyszerre.
Bővítések
Amennyiben nem elégszünk meg az ASP.NET output Cache működésével, a lehetőség nyitott, hogy
egyedi cache providert használjuk a 4.0-ás verzió megjelenése óta. Mivel ez a téma példakódok nélkül
elég nehezen bemutatható, az értelmes példakódok pedig elég terjedelmesek lennének, ezért egy
letölthető demót is mellékelő cikket tudnék ajánlani, ami egy fájl alapú output cache provider
implementálását mutatja be: ASP.NET 4.0: Writing custom output cache providers
Az output cache segítségével igen jelentős sebességnövekedést lehet elérni. Azzal, hogy az
oldalgenerálás végtermékét gyorsítótárazzuk, egy nagy huszárvágással meg tudjuk oldani a lassulást
okozó problémák egy részét. Előfordulhat azonban, hogy a request paraméterek olyan nagy
kombinációs halmaz szerint kéne cache-elni (varyby…), hogy értelmetlenné válik a kész oldalak
gyorsítótárazása, mert gyakorlatilag esélyes, hogy minden oldallekérés egyedi lesz. Olyan helyzet is
előfordulhat, hogy az oldalak cache variánsai nem csak a request nyers paramétereitől függenének,
hanem a paraméterek és az adatbázisból érkező adatok együttes értelmezéséből lesznek mások és
mások. Esetleg a cache variánsok jól definiálhatóak, de a cache találati értékei50 azt mutatják, hogy
gyakorlatilag nincs is kihasználva a rendszer.
Az olyan helyzetben, amikor az output cache használata kétes eredményt hozna, még mindig ott van
a System.Web.Caching.Cache, hogy a szintén igen költséges előállítású modelladatainkat
gyorsítótárazhassuk. Ez a Cache objektum elérhető a HttpContext objektumból, de az MVC nem ad
támogatást a használatára. Azért, hogy mégis megnézhessük a használatát, MVC közelivé tesszük a
következő példák során egy action filterrel, amik inkább a filterek, az MVC és a cache kapcsolatáról
szól, mintsem kész, tökéletes megoldásról. Tehát lehet még rajta faragni.
Egy általános action metódus belseje úgy szokott kinézni, hogy a paraméterek alapján egy modellt
készítünk, és ezt kapja meg a View. A dataService egy adatszolgáltatót jelent, aminek a metódusaival
lehet az adatokat, a modellt lekérni.
50
(Cache hit) Egy időszak alatt, mennyi hasznos kiszolgálás történt a cache-elt adatok alapján. Például, ha 10 perc
alatt a cache bejegyzések jelentős részének újrahasznosítási értéke 1 körül van, akkor a cache-elés csak a
memóriát fogyasztja, használata megkérdőjelezhető.
10.2 Reakcióképesség, gyorsítás, minimalizálás. - Az adat Cache 1-307
Így minden egyes keresés újra és újra az adatbázist/szolgáltatást zaklatja. Hasonlóan az output
cachenél, a category és a name értéke alapján lehet cache kulcsokat képezni, és minden egyes keresési
variáció eredményét eltárolhatjuk a Cache-ben. Ha új keresés érkezik, előbb megnézzük, hogy a
közelmúltban volt-e előállítva modell ugyanilyen paraméterértékekkel és az adatbázis lekérdezés
helyett a modellt a Cache-ből vesszük elő. Következzenek a problémák.
A szóban forgó Cache szintén alkalmazás szintű, így nincs elszeparálva látogatónként, mint a Session.
Ez egy keresési helyzetben előnyös, mert mindegy, hogy melyik felhasználó miatt kellene a modellt
előállítani (feltéve, hogy nincs jogosultság-függésben). Megfontolandó, hogyha egy modelltípust több
action/kontroller is használ, akkor a különböző actionök, esetleg azonos nevű paraméterei mégis mást
jelentenek. Tehát lehet, hogy célszerű a cache kulcsot (indexet) actionön nevenként is megvariálni. A
legnagyobb probléma még is az, hogy mit csináljunk, ha a keresési feltétel szerint a modell tartalma
érvénytelenné vált. Például a cache-elt modell egy listát tartalmaz a termékekről az adott keresési
feltétel szerint. Ekkor egy új terméket hoz létre a termékmenedzser, ami a keresési feltételnek
egyébként megfelel. Ha ilyenkor a régi, cache-elt modellben levő listát szolgáljuk ki, akkor úgy fog
tűnni, hogy hiányzik az újonnan felvett termék. Tehát a cache-elt listát ki kell dobni, mert érvénytelen,
és újra fel kell építeni a modellt.
Az alábbi action metódus megvizsgálja, hogy van-e már a Cache-ből betöltött modell a ViewData-ban,
ha nincs, feltölti azt.
[ModelDataCacheFilter("CacheDemoModel-forTest", "id")]
public ActionResult CacheTest(int? id)
{
if (ViewData.Model == null) //nem volt a cache-ben
ViewData.Model = CacheDemoModel.GetModell(id ?? 1);
return View();
}
A ModelDataCacheFilter attribútum két paramétert vár: az első a cache kulcs előtagja, amivel
igazából a modell felhasználási területeit tudjuk megkülönböztetni. Ez védi ki azokat a
hibalehetőségeket, ami abból adódna, ha a modell típust több teljesen más célú actionök is
használnák. A második paramétere az a request vagy routedata érték, ami szerint el szeretnénk
különíteni a cache-elt modelleket. Ez lenne az output cache "varyby" megfelelője.
}
}
Az actionfilter konstruktora a vesszővel elválasztható action paraméterekből egy listát készít. Illetve
eltárolja a model cache-kulcs előtagot.
Az action futása után az OnResultExecuted-re kerül a sor. Ez kész modellt kap, és ha a cache-kulcs
alapján nem talál cache bejegyzést, akkor eltárolja a modellpélányt a Cache-be. (A tárolás módjáról
még lesz szó).
A működés sarkokköve a "Kulcskészítő" GetCacheKey metódus. Mivel az attribútumot úgy írtam meg,
hogy ne legyen kötelező az első paraméter megadása, ezt valahogy pótolni kell ilyen esetben.
[ModelDataCacheFilter(null, "id")]
Ha tehát nincs megadva, akkor a kontroller és az action nevéből képez előtagot. A következő
lépésben a megadott (kötelező) második paramétert megpróbálja megkeresni két helyen is. A route
adatok és a request adatok között . Erre a kettőre azért van szükség, mert a kontroller/action/id
jellegű URL esetén az "id"-ből nem képződik bejegyzés a Request objektumban. Illetve nem képződne
bármely más route map által lefedett paraméterből sem. A Request objektumból viszont megkapjuk
az URL paramétereket (kontroller/action/id?catagory=Butorok). De hogy ne legyen egyszerű az élet
az "id" megérkezhet post request esetén a Request objektumban is. Ezért készítettem el úgy a cache
kulcs képzést, hogy a kettő unióját veszi.
Post estén a OnActionExecuting azért nem a ViewData.Model-be másolja a cache-elt modelt, mert
akkor összeütközésbe kerülne a model binder által a post adatokból feltöltött modellpéldánnyal. A
következő action a post requestre reagál. (Az előbbi probléma nem érinti, mert nincs
CacheDemoModel típusú metódusparamétere, de akár lehetne is).
10.2 Reakcióképesség, gyorsítás, minimalizálás. - Az adat Cache 1-310
[HttpPost]
//[ModelDataCacheFilter(null, "id")]
[ModelDataCacheFilter("CacheDemoModel-forTest", "id")]
[ActionName("CacheTest")]
public ActionResult CacheTestPost(int? id)
{
if (!id.HasValue) return RedirectToAction("Index");
CacheDemoModel originalModel = ViewData["originalModel"] as CacheDemoModel;
if (originalModel ==null)
originalModel = CacheDemoModel.GetModell(id.Value);
if (TryUpdateModel<CacheDemoModel>(originalModel)) {
//Ide jön a "Mentés az adatbázisba" funkció
return RedirectToAction("Index");
}
return View(originalModel);
}
Nem állítom, hogy ez egy best-practise51 megoldás, viszont szemlélteti, hogyan lehet ide-oda
passzolni a modellt az actionfilter, az action és a View között, úgy hogy a Cache-t is kipróbáltuk. A
Cache-t más rétegekben és a feldolgozás más pillanataiban is fel lehetett volna használni, hogy
hasonló célt érjünk el. Egy lehetséges változat, hogy olyan model binder-t készítünk, ami annál a
pontnál, amikor egy új modellt kéne példányosítania, a példányosítás helyett a Cache-ből veszi a kész
példányt post request esetén. Egy mégjobb és közös megoldás, hogy a Cache-t nem az actionből vagy
az actionfilterből kezeljük, hanem a modelleket egy repository/factory rétegből kérjük el, ami
belsőleg kezeli a Cache-t. Ebben az esetben viszont már nem ezt a System.Web.Caching.Cache-t
érdemes használni, ami webalkalmazásfüggő, hanem a .Net 4 óta elérhető
System.Runtime.Caching.MemoryCache-t.
A most kipróbált Cache-t először ASP.NET 2.0 Web Forms alkalmazásban használtam, tehát nagyon
régi, azóta sem sokat változott. Ahogy láttuk a kezelése nagyon egyszerű, csak két sorra volt
szükségünk. A Cache-be töltésre és a kivételre.
... =Cache[cachekey]
Az Insert metódus helyett használhatjuk az Add-ot is. Mindkét változatnál a paramétereivel lehet
testre szabni az adott cache elem viselkedését. A kiindulási pont legyen a legtöbbparaméteres
változat:
key – A cache elem egyedi azonosítója. Általában úgy szokták összekonkatenálni az alapján,
hogy milyen jellemző értékek szerint lett a tárolandó objektum összeállítva. "XXX_YYY_ZZZ".
value – Az objektum, amit tárolni szeretnénk.
51
http://msdn.microsoft.com/en-us/library/aa478965.aspx - ASP.NET Caching: Techniques and Best Practices
10.2 Reakcióképesség, gyorsítás, minimalizálás. - Az adat Cache 1-311
Mivel független cache elemet nem tudunk tárolni, ezért a dependencies vagy az absoluteExpiration
vagy a slidingExpiration közül az egyiket meg kell adni. A Cache-nek egy nagyon fontos jellemzője,
hogy nincs garantálva az, hogy a lejárati idő végéig a cache elem elérhető marad. Elfogyó szabad
memória esetén a Cache felszabadításra kerülhet.
10.3 Reakcióképesség, gyorsítás, minimalizálás. - A Bundling 1-312
10.3. A Bundling
Egy nagyobb webalkalmazás jellemzője, hogy több kész front-end modult használ fel, amik JS és CSS
fájlok formájában kapcsolódnak az oldalaikhoz. A téma szempontjából lényegtelen, hogy ezek gyári
modulok vagy a cégen belüli fejlesztések. Ha még nem sokat foglalkoztunk ilyenekkel, akkor képzeljünk
el sok-sok JS és CSS fájlt. Egyébként nem is kell ilyen messzire menni, hisz az Visual Studio template-el
generált project is hemzseg az ilyen kiegészítőktől. Nagyon valószínű, hogy egy kis alkalmazásnál is
szükségünk lesz a jQuery-re, esetleg a modernizer.js-re, jQuery UI-re, a jQuery.validation-ra, stb.
Ahhoz, hogy a böngészőben megjelenjen egy oldal, ezeket a JS és CSS fájlokat is le kell kérnie a
szerverről. Ami igen időpazarló, mivel a kapcsolat felépítése, a szerver munkaideje, a letöltési idő mind
összeadódik és megszorzódik a fájlok számával. Tegyük fel, hogy egy kis alkalmazásunk van és csak
20db további fájlt kellene letöltenie a szerverről. Vegyünk egy fájl letöltési idejét mondjuk átlag 50ms-
ra, akkor egy másodperc eltelik ezzel. Míg ha egyben töltenénk le az egészet, akkor esetleg a fele ideig
tartana. Általában jellemző, hogy ezeknek a moduloknak saját fejlesztési ciklusuk van, ezért nem
célszerű ezekbe belenyúlni, a verziófrissítések követése miatt. Még úgy sem, hogy esetleg egy nagy
fájlba fésüljük össze ezeket. Ahogy az alkalmazásunk nő, fejlődik és korosodik, vélhetően egyre több
és újabb javascript framework plugin jelenik meg bene. Egy idő után kezelhetetlenné válna a kézi
"montírozás".
Az ilyen sok JS + sok CSS –t használó alkalmazásokat megfigyelve azt tapasztalhatjuk, hogy az oldalak
jelentős része azonos JS modulokat és CSS fájlokat használ. Valószínűleg fogunk találni némi variációt
a szükséges fájlcsoportok között, de az a tapasztalat, hogy az oldalak jelentős részénél csak 1-5
elkülönülő fájlcsoportot tudunk azonosítani.
Némi kompromisszum és szervezés után a négy oldal vagy oldalcsoport esetén összesen két JS
modulcsoportot találtam. A kompromisszum, hogy a "Bemutatkozó oldal" nem igényel jQuery UI-t,
mégis megkapja. Ennek nincs jelentősege ebben az esetben, mert vélhetően a "Nyitó oldal" után
navigál erre az oldalra a látogató. A "Nyitó oldal" esetén pedig a böngésző letöltötte és el is cache-elte
a komplett jquery_ui modulcsoportot. Tehát, hogyha a JS könyvtárak fájljait egybe tudjuk gyömöszölni,
egy kis előnyt szerezhetünk, mivel sok fájl helyett csak egyet kell letöltenie a böngészőnek.
Egy másik probléma, hogy a JS és a CSS fájlok szövegalapú kódfájlok, amik az emberi faj számára
olvasható formájukban, nagyon sok felesleges karaktert tartalmaznak. A szóközök és a soremelések
mellett ott vannak a hosszú változó- és funkciónevek is. Amikor ezektől a sallangoktól megszabadítjuk
ezeket a fájlokat az eredmény egy 50-70%-os fájlméret lehet az eredetihez képest. Ennek a módszernek
a szép magyar neve a: minifikálás52, ami az angolból jött, ott "minification"-ként hivatkoznak rá. Az
ilyen módszerrel aszalt/zsugorított fájlok nevében, kiterjesztésében, de-facto szabványként ott találjuk
a "min" rövidítést. A projekt Scripts mappájában találunk a legtöbb JS fájlból két verziót is. A normált
.js és a minifikált .min.js kiterjesztésű változatát.
52
A mummification - mumifikálás analógiájára
10.3 Reakcióképesség, gyorsítás, minimalizálás. - A Bundling 1-313
A két változat mellett látható még a –vsdoc.js végződésű verzió is, ami a Visual Studio intellisense
számára tartalmazza a funkciókhoz és változókhoz tartozó kommenteket. Többet ér ezer szónál, ha
megnyitjuk ezeket a fájlokat. Rögtön világossá válik minden. Ha már itt tartunk, a JS fájlok közti
_references.js magic fájl tartalmazza azoknak a JS fájloknak a felsorolását, amihez szeretnénk igénybe
venni ezt az intellisense szolgáltatást bármelyik megnyitott View/html szerkesztőben. Azért van benne
többek között ez a sor is, hogy a névben passzoló jquery-validate-vsdoc.js fájlt felolvassa a VS:
Már csak az a probléma, hogy jó lenne, ha a fejlesztési időben a debuggolás alatt ne a minifikált
változatot használja az oldalunk, hanem az ember számára is olvasható formájút. A release verzióban
viszont pont fordítva van. A kisméretű, összeláncolt JS csomag menyjen ki a felhasználók böngészőjébe.
Ezeken a problémákon segít a bundling, amit eddig is csokornak 53 fordítottam, a sajátos működése
miatt. Az eddigi fejezetekben már sokszor használtuk ilyen formában:
@section Scripts {
@Scripts.Render("~/bundles/jqueryval")
}
Két fajta bundle érhető el. A ScriptBundle, ami javascript fájlok összefűzéséhez való és a StyleBundle,
ami a CSS fájlokat tudja csokorba foglalni. Az Visual Studio projekt templatet véve példának, az
App_Start mappában található BundleConfig.cs majdnem mindent elárul a működéséről és a
beállításáról. Nézzük meg ennek az első két definícióját és, hogy mi-mit jelent:
bundles.Add(new ScriptBundle("~/bundles/jquery").Include(
"~/Scripts/jquery-{version}.js"));
bundles.Add(new ScriptBundle("~/bundles/jqueryui").Include(
"~/Scripts/jquery-ui-{version}.js"));
A konstruktorparamétere egy virtuális path-t vár. Ez nem azt jelenti, hogy a webalakalmazásunk
mappaszerkezetében lenne egy /bundles/jquery fájl, ez csak egy szimulált út. A @Scripts.Render pedig
erre a virtuális path-ra fog hivatkozni, mert ezen az útvonalon lehet majd elérni az összefűzött JS
fájlokat. Ennek a stringnek van egy harmadik célja is, hogy Cache index legyen (a névkártya a
virágcsokorban). Ez azt jeleni, hogy az összefűzött fájlokat a memóriában tárolja az MVC.
53
A virágcsokrot nem csak "kötegelik", hanem kiválogatják, összefűzik, csomagolják, lemetszik a felesleges
részeket. Esetleg kiszárítják. Egy kis címkét/kártyát is adnak hozzá némi jókívánságokkal.
10.3 Reakcióképesség, gyorsítás, minimalizálás. - A Bundling 1-314
Az Include metódusa egy string param tömböt vár azoknak a JS fájlok valódi elérési útjával, amiket
össze szeretnénk fűzni. Például, ha tudjuk, hogy minden oldalunkon használni fogjuk a jQuery alap
keretrendszer mellett az UI-t is, akkor egybe lehet gyúrni a jQuery alap framework-kel:
bundles.Add(new ScriptBundle("~/bundles/jqueryfull").Include(
"~/Scripts/jquery-{version}.js",
"~/Scripts/jquery-ui-{version}.js"));
A felsorolás sorrendjének követnie kell az egymásra épülő JS kódok logikáját. Tehát a jQuery UI-nak a
jQuery után kell következnie, mert hivatkozik az alap keretrendszerre. Látható, hogy lehet verzió
szakaszokat {version} kijelölni a fájlnévből, amit úgy értelmez, hogy mindegy milyen verziószám áll a
fájlnévnek ezen a pontján. Ez akkor hasznos, ha követni akarjuk az újabb kiadásokat, de anélkül
szeretnénk azokat rendszeresen lecserélni a Scripts mappában, hogy a bundling-ban is aktualizálni
kéne a verziószámokat.
Lehetőség van az egzakt fájlnév megnevezés helyett * wildcard-ot is használni. Erre szintén ott a példa
a BundleConfig.cs-ben, ami a jQuery validációs függvényeket gyűjti egybe:
bundles.Add(new ScriptBundle("~/bundles/jqueryval").Include(
"~/Scripts/jquery.unobtrusive*",
"~/Scripts/jquery.validate*"));
Most az következne, hogy próbáljuk ki, de még előbb be kell kapcsolni. Azért, hogy a fejlesztés során
debuggolható legyenek a JS kódok, a bundling rendszer nem csinál mást, mint normál linkkel csatolja
az oldalhoz az Include(..) paraméterében felsorolt fájlokat, értelmezve a {version} szakaszokat. Ebben
nincs semmi különös. Az előbbi "~/bundles/jqueryfull" nevű csokrot így generálja a HTML-be debug
módban:
<script src="/Scripts/jquery-1.8.2.js"></script>
<script src="/Scripts/jquery-ui-1.9.2.js"></script>
Két módon lehet bekapcsolni a teljes funkcionalitást. Az egyik, hogy a web.config-ban kikapcsoljuk a
debug fordítási módot, mivel ez azt feltételezi, hogy az alkalmazás éppen fejlesztés alatt van:
<system.web>
<compilation debug="false" targetFramework="4.0"/>
BundleTable.EnableOptimizations = true;
Ezt a sort az alkalmazás indulásakor kell érvényre juttatni. Talán a legjobb hely számára a
RegisterBundles metódus vége. A működés eredménye, hogy egy ilyen markup szelet jelenik meg a
HTML-ben:
<script src="/bundles/jqueryfull?v=qYVvTzcCR32NkdMA13cs5v3BMnIgax5T9BJ2y01qz6U1"></script>
A hosszú token az URL végén egy verziószám-szerűség, ami a böngésző számára szól. Az összefűzött JS
tartalom, mint fájl érkezik meg, és ezt a komplett URL alapján cache-eli a kliensen, egy éves lejárati
idővel. Ha a szerveren megváltozik a bunlingben levő JS fájlok tartalma, például mert új JS verziók
kerültek bele, akkor új tokent kap. A tokenváltás azt eredményezi, hogy az előzőleg helyileg cache-elt
változat már érvénytelen és ezt az új verziót kell használnia a böngészőnek. A token egészen pontosan
egy hash kód, ami az összefűzött scriptek szöveges tartalmából készül, ami miatt a legkisebb JS
tartalomváltozás is eltérő tokent fog eredményezni.
Nem mellékesen, a teljesen aktív bundling képes arra is, hogy az Include() metódusban felsorolt fájlok
".min", minifikált verzióját használja, ha létezik ilyen JS fájl. Ha van lehetőségünk rá, az éles
programváltozathoz szerezzük be a "gyárilag" minifikált változatot, mert az a biztos. Egy utalás
található a BundleConfig.cs fájlban a http://modernizr.com–ra, hogy a producion változathoz állítsuk
össze azokat a funkcionalitásokat, amikre valóban szükségünk van. Ugyan ezt a lehetőséget láthatjuk
a jQuery UI hivatalos letöltési oldalán. A nem használt képességek kihagyásával további jelentős JS
fájlméret-csökkenést lehet elérni.
A másik, ami nem mellékes, hogy ha nincs ilyen minifikált verzió, mert mi írtuk a szkripteket, akkor
megcsinálja magától is a minifikálást. Igaz, ezt nem menti el, mint fájlváltozatot, de a memóriában
(Cache-ben) ez a változat tárolódik és kerül kiküldésre a kliensnek. Azonban nem árt óvatosnak lenni
az összefűzött és automatikusan minifikált JS tartalommal. Elvileg nem okozhat gondot, főleg ha a JS
fájlok sorrendjére is figyelünk, mégis előfordulhat, hogy az összefűzés nem várt eredményeket
okozhat. Ezért érdemes egy statikus unit-tesztoldalt fenntartani a használt JS függvények számára,
hogy minden, de legalább a lényeges függvények működnek-e. A probléma nem is annyira a JS
keretrendszerekkel fordul elő, hanem az általunk egyedileg az oldalakhoz kapcsolt JS fájlokkal. Azt
tartja az ajánlás, hogy az saját kezűleg írt JS kódot ne a View .cshtml fájljában írjuk meg, hanem tartsunk
fenn erre egy külön (azonos nevű) kódfájlt. Persze ezt lehet összevonni és saját
függvénygyűjteményeket használni, ezzel is lehet méretet és fájl mennyiséget spórolni. A sok különálló
saját JS fájl összefésülése akkor okozhat problémát, ha a függvénynevek és az objektumnevek
átfedésben vannak egymással. Ez addig, amíg nincs egységes JS fájlunk ez nem lesz zavaró. Ezért a
bundling használatát, már a projekt indulásakor érdemes betervezni és figyelembe venni. Mivel a sok-
sok különálló JS fájlt végül egy fájlban fogjuk felhasználni, célszerű valami névkonvenciót/névteret
használni a funkciókhoz.
A legnagyobb, kész JS framework-ök elérhetőek un. Content Delivery Network (CDN) szerverekről is.
Így például a könyv írásakor a jQuery elérhető volt a http://code.jquery.com/jquery-1.10.1.min.js URL-
ről és még számtalan más hivatalos CDN URL-ről. A Microsoft, Google is szolgáltatja a saját CDN-jén
keresztül. A CDN használatának három nagy előnye van:
Nagyon gyorsan szolgálják ki a kéréseket. Minden bizonnyal a jQuery előbb említett URL-jéről
most is nagyon sokan kérik le a JS fájlt így "bekészletezve" szolgálja ki a mi kérésünket is.
Valószínű, hogy a köztes proxy szerverek erre még rátesznek egy kicsit a gyorsításban.
A böngészők úgy vannak belőve, hogy párhuzamosan maximum hat kérést indítanak azonos
domainű kiszolgáló felé. Amíg ezekre nem érkezik meg a válasz, a további fájligényeiket
várakoztatják. Emiatt is komoly létjogosultsága van, hogy egy teljes oldallekérés kevés további
fájlokat igényeljen egy azonos doman alól (egy webszerverről). Azonban, ha a további JS, CSS
és kép fájlokat más domain nevű URL-en tárolunk, akkor a böngészőnek nem kell annyit
várakoznia, párhuzamosabban tudja beszerezni a linkelt tartalmakat.
Nem a mi szerverünket és sávszélességünket terheli a lekérés.
10.3 Reakcióképesség, gyorsítás, minimalizálás. - A Bundling 1-316
A bundle is képes CDN használatára. Ráadásul úgy, hogyha a CDN nem lenne elérhető, a
vésztartaléknak beállított saját szerverünkön tárolt változatot szolgálja ki.
bundles.Add(new ScriptBundle("~/bundles/jqueryfull",
"http://code.jquery.com/jquery-1.10.1.min.js").Include(
"~/Scripts/jquery-{version}.js"));
Ennek a működésnek a sajátossága, hogy a framework-öket szolgáltató hivatalos tárhelyek nem tudnak
a mi összefűzési igényünkről, így ha ezeket használjuk, csak egyesével tudunk rájuk hivatkozni
kötegelve nem. Ahhoz, hogy kötegeljük, nekünk kell egy - egyébként fizetős - CDN tárhelyre
feltöltenünk a tartalmat. A másik jellemzője, hogy a valódi CDN URL-t csak release módban használja.
Debug módban a helyi webszerverről a tölti le a fájlokat.
Az eddig tárgyalt ScriptBundle a javascript fájlokhoz való. Mint említettem a CSS fájlokhoz a
StyleBundle osztály passzol. A kettő között a különbség a tömörítés módjában van, amit magukon belül
definiálnak ezek az osztályok. Egy JS kódot teljesen más módon kell minifikálni, mint egy CSS stílus fájlt.
Viszont a két változat paraméterezése megegyezik.
bundles.Add(new StyleBundle("~/Content/css").Include("~/Content/site.css"));
Személyesen még nem tapasztaltam hibát a CSS minifikálás eredményével, de mivel már láttam ilyen
irányú problémafelvetéseket, amik hol reprodukálhatóak, hol nem, itt is célravezetőnek tartom a
tesztelést. Sajnos a CSS tesztelése nem egyszerű, mivel a CSS stílusok a böngészőben értékelődnek ki,
így marad a "szemrevételezés" vagy egy automatizált tesztelési eszközt vetünk be, mint például a
Selenium54-ot.
A web optimalizálás kérdésköre nem áll meg a JS és CSS fájlok minimalizálásánál és a CDN
használatánál, a beépített ASP.NET + MVC képességek viszont igen. Létezik még az oldalon megjelenő
sok kis képek problémája, ami jóval nagyobb gond lehet, mint a CSS és JS optimalizálás együttvéve.
Minden egyes kis (16x16 - 48x48) képecskéért, amit külön URL-segítségével töltünk le például egy
<img> HTML taggel vagy egy background-image CSS stílus paramétereként, egy teljes requesttel
fizethetünk. Erre a legjobban bevált módszer, hogy a kis képeket egy nagy képre mozaikozzuk rá, amit
CSS sprite-nak 55 neveznek. Könnyen automatizálható, amíg nagyjából egyforma méretű és típusú
(fájlformátum, színmélység) kis képeket használunk, hogy ezeket egy mappából felolvassuk és
összeillesszük. Viszont, ha nem ilyen idilli a helyzet, akkor a generikus algoritmizálás valahogy nem válik
be, mondjuk jelentősen eltérő képméretek és képtípusok esetén. Ehhez még hozzájön, hogy a képekre
hivatkozó CSS stílus definíciókat is aktualizálni kell (background-position). Ezért még mindig van
létjogosultsága egy manuális, félig automatizált sprite készítésnek. Ez megéri a fáradságot, ha nagyon
sok apró grafikai elem van a HTML oldalainkon.
Egy másik igény, ami elég gyakran előkerül webfejlesztés során, hogy jó lenne, ha nem csak a HTML
fájl előállítása lenne sablon alapú, hanem a CSS tartalma is. Vagy, ha nem is sablon alapú, de legalább
változókat, stílusöröklődést, némi programozhatóságot, dinamizmust tartalmazzon. Nálam szinte
minden CSS tartalmazott ismétlődő stílusokat, legfőképpen a színek esetében. Ezek megváltoztatása
nem biztos, hogy megoldható text replace módon, márpedig a megrendelő valahogy mindig az ilyen
globális definíciókat akarja megváltoztatni: "Nem lehetne minden betű színe kicsit világosabb?". Erre
54
http://docs.seleniumhq.org/
55
http://www.w3schools.com/css/css_image_sprites.asp
10.3 Reakcióképesség, gyorsítás, minimalizálás. - A Bundling 1-317
született néhány jó megoldás LESS és SASS néven. Például egy szín konstans definiálása és
felhasználása ilyen egyszerű:
@brand_color: #4D926F;
#header {
color: @brand_color;
}
h2 {
color: @brand_color;
}
A @ most nem razor kódot vezet be.
BundleTable.EnableOptimizations = true;
A kódrészletet a bundle normál konfigurációi közé tehetjük.
A LESS fordító a .less kiterjesztésű fájlokat használja. A StyleBundle így most a "mini.less" fájlt fogadja,
de azzal, hogy a belső Transforms listájának a nulladik helyére szúrtunk be egy LessTransform példányt,
azt értük el, hogy ennek a feldolgozását előbb a LESS fordító fogja elvégezni és csak utána a CssMinify,
az eredeti CSS minifikáló.
using System.Web.Optimization;
using dotless.Core;
namespace MvcApplication1
{
public class LessTransform : IBundleTransform
{
public void Process(BundleContext context, BundleResponse response)
{
response.ContentType = "text/css";
response.Content = Less.Parse(response.Content);
}
}
}
10.3 Reakcióképesség, gyorsítás, minimalizálás. - A Bundling 1-318
#header{color:#4d926f}h2{color:#4d926f}
A felhasznált Dotlesscss NuGet csomag a telepítéskor a gyökér web.config fájlba HTTP handler
regisztrációkat helyez, amire ebben az esetben nincs szükségünk, mert a .less fájl transzformálását
most a Bundling rendszer végzi el. A handlerekre akkor lenne szükség, ha nem használnánk a bunlde
képességeit és direkt .less fájlokra hivatkoznánk a HTML fejlécben, például így:
<httpHandlers>
<!--<add path="*.less" verb="GET" type="dotless.Core.LessCssHttpHandler, dotless.Core" />-->
</httpHandlers>
<handlers>
<!--<add name="dotless" path="*.less" verb="GET" type="dotless.Core.LessCssHttpHandler,dotless.Core"
resourceType="File" preCondition="" />-->
</handlers>
11.1 Real world esetek - Többnyelvű alkalmazás 1-319
A következő fejezetekben olyan témákat szeretnék bemutatni, amik az eddig tanultakat kicsit
továbbmélyítik, ismétlik és a témákat kapcsolatba hozzák egymással, ahogy ezekkel majd a valódi
helyzetben is találkozhatunk. Lehetőségek felvillantása olyan szituációkra, amikor az alkalmazásunk
elér egy nagyobb komplexitást, esetleg nyitni szeretnénk szélesebb, többnyelvű, mindenféle kütyüket
használó közönség számára.
Nekünk, magyar nyelvűeknek ez egy fontos téma, azonban elég elszomorító, hogy web technológiákkal
foglalkozó szakkönyvek (MVC is) jelennek meg tucatjával, amik még utalást sem tartalmaznak a
többnyelvű alkalmazások felépítésére. Számomra szintén megdöbbentő, hogy az alaptechnológiákra
épülő alkalmazások, kiváltképp CMS-ek belső felépítésében csak mellékszerep jut a valódi többnyelvű
megvalósításnak.
Amikor többnyelvű alkalmazásról beszélünk érdemes pontosítani, hogy mit értünk alatta és mit ért
alatta a megrendelő. Mi legyen/lehet többnyelvű?
A .Net megjelenése óta rendelkezésre áll a módszer, hogy a nyelvenként elkülönülő erőforrásokat
kezelni tudjuk a .resx kiterjesztésű resource fájlokon keresztül. Ez lehetőséget nyújt képek, szövegek,
ikonok nyelvenként elkülönülő tartalmának a tárolására. Most mi csak a szövegekre koncentrálunk. A
Display attribútumnál előkerült a használata, ahol a mezőfeliratot úgy tudtuk meghatározni, hogy a
típusos resx definíció bejegyzésére hivatkoztunk. Emlékeztetőként:
Abban a példában éppen csak azt néztük meg, hogy ezt meg lehet csinálni, de ez így csak arra elég,
hogy egy helyen tároljuk a mezőfeliratokat és a validációs üzeneteket, ami még nem többnyelvűség.
Az így elsőnek létrehozott resource fájl képezi a szövegek alapértelmezett tartalmát. Minden további
nyelvi verzióhoz létre kell hozni egy nyelvspecifikus resource fájl változatot. A fájl névkonvenciója az,
hogy az eredeti fájlnév mögé, de a .resx fájlkiterjesztés elé be kell írni a nyelv kódját.
A nyelvi kódok sok esetben kéttagúak az első tag (pl. en) jelenti a nyelv alapkódját a kötőjellel
elválasztott második tag (-GB) az országkódot, ahol az adott nyelvi változatot beszélik. Ennek a magyar
nyelvnél nincs jelentősége addig, amíg egy elzárt kis afrikai közösség politikai okokból nem gondolja
azt, hogy állami nyelvé nem teszi ékes nyelvünket. Egy spanyol, vagy angol nyelvnél viszont fontos
lehet, hogy meghatározzuk melyik nyelvjárást szeretnénk használni. Ilyen nyelveknél, ha csak az első
taggal határozzuk meg a resource fájl nevét, akkor az az összes országkódot jelenti. Így nem teszünk
különbséget az brit, az amerikai és egyéb angol esetén, ha a fájl neve "UILabels.en.resx". Míg az
"UILabels.en-GB" a brit angolt jelenti. Azonban ez még mindig nem elegendő minden helyzetre. Nyitva
hagyom a kérdést, de mi van akkor, ha a rendszert úgy akarják használni - hogy amelyik nyelvben
11.1 Real world esetek - Többnyelvű alkalmazás 1-321
lehetséges - legyen egy formális és egy informális nyelvi változat is a különféle korosztályoknak,
ügyfélcsoportoknak?
A további nyelvi változatokat tároló resource fájlokba csak azokat a definíciókat kell átmásolni, amit az
alapértelmezett verzióban levőből le is szeretnénk fordítani. Azaz általában mindent. Viszont, ha nem
másoljuk át mindet és így az erőforráskezelő nem fogja megtalálni a nyelvspecifikus resource
bejegyzést, akkor az alapértelmezett resource fájlból fogja venni a bejegyzéshez tartozó szöveget
(fallback).
A resource fájl bejegyzései, a Visual Studio jóvoltából, típusosan elérhetővé válnak a resource fájlból
automatikusan készülő osztály segítségével. Ezek találhatóak a resourcenév.Designer.cs fájlban. Íme,
egy lecsupaszított lényegi kivonat:
namespace MvcApplication1.Resources {
using System;
@MvcApplication1.Resources.UILabels.FullNameLabel
A .Designer.cs fájlnak csak az alap nyelvi verzióban lesz tartalma, a többi mint például a
UILabels.en.Designer.cs üres marad. A példát kicsit elrontottam, mert ajánlatosabb az angolt, mint
világnyelvet megadni alapértelmezésnek és a magyart specifikusnak. Így az UILabels.resx –nek kéne
lennie az angolnak és jobb lett volna egy UILabels.hu.resx fájlt létrehozni a magyar szövegeknek. Végül
is nem baj, mert így eszembe jutott, mint ajánlás.
<system.web>
<globalization requestEncoding="utf-8" responseEncoding="utf-8" fileEncoding="utf-8"
culture="hu-HU" uiCulture="hu"/>
<system.web>
<globalization requestEncoding="utf-8" responseEncoding="utf-8" fileEncoding="utf-8"
culture="auto" uiCulture="auto"/>
Itt is felfedezhetjük a nyelvi kódokat. A sorrend és a "q" utáni érték alapján határozza meg az ASP.NET
az automatikus kultúra információkat. Azonban ez a nyelvi meghatározás sajnos nem mindig működik
megbízhatóan. Olyan helyzetek könnyen előállhatnak, hogy a felhasználó nem a saját gépéről
jelentkezik be, vagy nem jól van beállítva a preferált nyelv, esetleg az alkalmazásunk nincs felkészítve
arra a nyelvre, stb. Az ajánlás az, hogy kiindulópontnak jó az Accept-Language alapján érkező
információ, leginkább anonymous látogatók számára, akik most látják először az oldalunkat. Ha
tehetjük, inkább határozzuk meg a felület és az alkalmazás nyelvét felhasználónként, a profilja vagy
egy felhasználói döntés alapján. A felhasználói döntés miatt szinte kötelező jellegű, hogy valahol legyen
egy nyelvválasztó a felületen. A kiválasztott nyelvet utána egy hosszúlejáratú cookie-ban, az URL-ben,
vagy a profil adatok között eltárolhatjuk. Ezek sokkal célravezetőbb megoldások. A megvalósításuk
között nincs akkora különbség, mindössze néhány kérdést kell figyelembe venni.
Mi legyen akkor, amikor a felhasználó még nem állított be semmit? Honnan vegyük, hogy mi
az alapértelmezett nyelv?
A felhasználó beállításait hol tároljuk és meddig? Esetleg több helyen is tároljuk?
A nyelvi beállításokat a bejövő request feldolgozási folyamatában mikor, hol és hogyan kell
érvényre juttatni?
Mi legyen, ha az alkalmazásunk nincs felkészítve a bejövő 'Accept-Language' nyelvi kódra?
Az URL-ben tárolt nyelvi beállításokat egy speciális route bejegyzéssel le lehet fedni. Az egy más kérdés,
hogy ilyen esetben az összes route bejegyzést fel kell vértezni a nyelvi kód fogadására.
routes.Add("LocalizedRoute", langroute);
Ilyen esetben az URL vége például így nézhet ki: /hu/home/index. A LocalizedRoute egy Route
leszármazott és azért van rá szükség, hogy minden generált linkbe és Actionlink-be, be legyen
injektálva a nyelvi választó URL szakasz (/hu/ , /en/, /sk/). Nélküle a Home-ra mutató linkekből
hiányozna a nyelvi kód.
11.1 Real world esetek - Többnyelvű alkalmazás 1-323
Határozzuk meg melyek az alkalmazásunkban elérhető nyelvek és egy szűrőmetódust, ami csak az
elérhető nyelveket engedi át, a többit lecseréli az alapértelmezett "en"-re.
Egy kontroller is kell a nyelvválasztó linkek fogadására. Az Index action a példakódok kipróbálásához
készült View-t szolgálja ki. A ChangeLang a nyelvválasztó linkeket fogadó action.
Response.Cookies.Remove("lang");
var langcookie = new HttpCookie("lang", validlangcode)
{
11.1 Real world esetek - Többnyelvű alkalmazás 1-324
Expires = DateTime.Today.AddYears(1)
};
Response.Cookies.Add(langcookie);
if (Request.UrlReferrer != null)
{
this.HttpContext.RewritePath(Request.UrlReferrer.LocalPath);
var routeData = RouteTable.Routes.GetRouteData(this.HttpContext);
if (routeData != null && routeData.Values.Count != 0)
{
routeData.Values["lang"] = validlangcode;
return RedirectToRoute(routeData.Values);
}
}
A kód némi magyarázatot érdemel. A beérkező nyelvi kódot átengedi a szűrőmetóduson, hogy a nem
kezelt nyelvek ne okozzanak fennakadást. A következő szakaszban a responsba kerül a nyelvi beállítást
tároló cookie, egy éves lejárati idővel. Ha már létezett, akkor előbb törli. Az If-es szerkezetben levő rész
visszairányítja a böngészőt arra az oldalra, ahol a nyelvválasztó linkre kattintottak. Ha nem volt ilyen
megelőző link, akkor a nyitólapra irányít, úgy hogy a routeData speciális gyűjteménybe, a lang elembe
beleírja az aktuálisan érvényes nyelvi kódot.
Eddig már majdnem megvagyunk, csak még nem állítottuk be az aktuálisan futó szál nyelvét, azt ami a
request feldolgozását végzi. A resource kezelő és minden olyan szöveges formázó, megjelenítő,
konvertáló a futó requestet feldolgozó szál nyelvi beállításából veszi ki a kultúra információkat. Amint
láttuk a web.config beállításánál is két kultúra beállítás van a normál és az UI. Emiatt mindkettőt kezelni
kell. Az aktuális szál nyelvi információját az előtt át kell állítani, mielőtt az első nyelvspecifikus
feldolgozás, ToString() megtörténik, de azután, hogy a route adatok már rendelkezésre állnak. Erre a
legjobban bevált hely a global.asax BeginRequest eseménykezelője.
string usableLang;
var languageCode = (string)routeData.Values["lang"];
if (languageCode == null)
{
var langcookie = HttpContext.Current.Request.Cookies["lang"];
languageCode = langcookie != null ?
langcookie.Value :
System.Threading.Thread.CurrentThread.CurrentCulture.TwoLetterISOLanguageName.ToLower();
routeData.Values["lang"] =
usableLang = LanguageModel.GetAvailableOrFallback(languageCode);
}
else
usableLang = LanguageModel.GetAvailableOrFallback(languageCode);
System.Threading.Thread.CurrentThread.CurrentUICulture =
System.Globalization.CultureInfo.GetCultureInfo(usableLang);
System.Threading.Thread.CurrentThread.CurrentCulture =
System.Globalization.CultureInfo.CreateSpecificCulture(usableLang);
}
Ez a kód is három részre tagolódik. A felső szakasz megszerzi az aktuális route adatokat. Ezt a bejövő
URL feldolgozásának az eredményeként kapjuk meg. Az URL szakaszai leképződnek "controller",
11.1 Real world esetek - Többnyelvű alkalmazás 1-325
"action","id", és most már "lang" kollekció elemekre, amiket a routeData.Values-en keresztül lehet
elérni.
A 'lang'-nak a középső kódrészletben történik meg a lekezelése. Ha nem volt az URL-ben a "lang"-ra
leképezhető szakasz. Például: Home/Index, akkor megpróbálja a cookie-ból megszerezni. Itt megnézi
a kód, hogy a cookie-k között nincs-e "lang" nevű. Ezt a ChangeLang action állította be az en/hu/de
linek feldolgozásakor.
Ha nincs cookie sem, akkor megnézi, hogy a futó szálnak mi a nyelvi beállítása és ennek kiveszi a nyelvi
kódját. Ebben a pillanatban a futó szál nyelvi kódja a web.config globalization "auto" beállításai miatt
a böngésző nyelvi kódja szerint van beállítva. Mivel ez lehet akár kínai is, szintén át kell engedni a nyelvi
kódszűrőn. Mivel ebben az ágban a route "lang" értéke nincs beállítva, ezért ezt most meg kell tenni.
Ezt a beállítást fogja majd átvenni a LocalizedRoute osztály.
A végén megtörténik a futó szál nyelvének a beállítása, a normál és UI egyaránt. Ez volt a végcél. Az
eredmény a három nyelvválasztó linkre kattintás után:
hu en de
<table>
<tr>
<td>Current culture</td>
<td>@CultureInfo.CurrentCulture.Name</td>
<td></td>
</tr>
<tr>
<td>Current UI culture</td>
<td>@CultureInfo.CurrentUICulture.Name</td>
<td></td>
</tr>
<tr>
<td>Dátum formátum</td>
<td>@( DateTime.Now.ToString() )</td>
<td></td>
</tr>
<tr>
<td>Pénznem</td>
<td>@( (100.5).ToString("C") )</td>
<td></td>
</tr>
<tr>
<td>Tizedes jegyek</td>
<td>@( (100200.556).ToString("") )</td>
<td></td>
</tr>
<tr>
<td>Nev</td>
<td>@MvcApplication1.Resources.UILabels.FullNameLabel</td>
<td>@Html.DisplayFor(m => m.FullName)</td>
11.1 Real world esetek - Többnyelvű alkalmazás 1-326
</tr>
<tr>
<td>Cim</td>
<td>@Html.LabelFor(m => m.Address)</td>
<td>@Html.DisplayFor(m => m.Address)</td>
</tr>
</table>
@Html.ActionLink("Újratöltés","Index")
11.2 Real world esetek - Az alkalmazás modularizálása. Az Area. 1-327
Egy nagyobb webalkalmazásnál felfedezhetők (utólag ), vagy tervezetten (előre ) kialakíthatók
funkcionális csoportok, oldal szekciók. Legyen ez egy képzeletbeli, átlagos iskolának a webes
rendszere. Az egész rendszert felosztottam az alábbi szekciókra.
Nem tudom az olvasónak mekkora tapasztalata van egy webes rendszer felépítésében, de, ha
tisztességesen és trendin akarjuk megcsinálni amit a fenti táblázat vázol, az szekciónként is legalább
10 kontrollert jelenthet. Még ha belezsúfoljuk 5 kontrollerbe, akkor is tetemes mennyiségű View
mappát és benne levő View fájlt kellene az egy szem Views mappa alá betenni. Ha azonban nem
vagyunk oda ennyire a rendszerezésért és megfelel egybe, ahogy van, gondoljunk a következőkre.
A View-k egy közös Shared mappán fognak osztozni. Ebbe kerülnek a közös nyitólapi partial
View-k, de az Adminisztrátori oldalak és a Diákélet és az összes többi szekció partial View-jai
is.
Kényszerűen ki fognak alakulni szoros, de felesleges függőségek olyan szekciók között,
amiknek semmi közük egymáshoz. A függőségek meg fognak jelenni a közös kontroller-
szolgáltatásokban (menükezelés, felhasználó kezelés), a közös adatelérési rétegekben,
szolgáltatáshívásokban. Ez később, a kód karbantartásánál az alkalmazás bővítésénél majd jól
megbosszulja magát.
11.2 Real world esetek - Az alkalmazás modularizálása. Az Area. 1-328
Előbb vagy utóbb megjelenik a megrendelőtől az igény, hogy a szekciók más és más dizájnnal,
elrendezéssel jelenjenek meg. Ekkor szekciónként külön _Layout.cshtml fájlt kéne csinálni.
Utána meg karbantartani mindet.
A szekciók között igen jelentős látogatottság különbség várható és a teljesítmény igény is
eltérhet nagyságrendekkel. Az adminisztrációs oldalakat nyilván sokkal ritkábban és
kevesebben használják, mint a Diákélet pörgős tartalmait. Valószínű, hogy a Diákélet szekció
sok interaktivitást, javascript kódot, képet fog tartalmazni.
Sőt elképzelhető, hogy eltérő külső komponenseket fognak használni, amik a sajátos beállítási
és környezeti igényeik miatt (sok-sok javascript) jól megkavarnak mindent. Lehet, hogy a többi
oldalon ezek zavaróak lennének, míg egyes szekciókban kihagyhatatlanok.
Az alkalmazás nem lesz modulárisan fejleszthető. Nem egyszerű fejlesztési munkacsoportokat,
ütemterveket, részleges átadásokat képezni. Mindig össze kell fésülni a teljesen eltérő
működésű szekciós kódokat, megjelenítési elemeket, stílusokat. Mivel ennyire egybe van
mosva minden, sokkal nagyobb a valószínűsége, hogy egy apró, akárcsak a CSS-ben elkövetett
hiba az egész alkalmazás működését elrontja.
Szó, ami szó, tudnám még sorolni, hogy milyen problémák jelenhetnek meg, ha egy nagy egybefüggő
MVC alkalmazást készítünk. Nézzük inkább a megoldást. A szekciókat innentől Area-nak fogjuk hívni,
ezek lesznek az alkalmazás "szakterületei".
A Solution Explorerben a projekt nevén egy helyi menüt kérve lehet egyszerűen Area-t létrehozni.
Az Areas mappa alatt létrejött egy Admin nevű area, benne a megszokott
MVC struktúrával. Ugyan úgy megvan a View-s mappa, alatta Shared
mappával és a web.config-al. Van saját Controllers, Models mappája. Ide
tehetjük az alkalmazásszekcióhoz kapcsolódó kontroller és modell
definícióinkat. Mivel ez egy valódi mappaszerkezet, lehetőség van például
Content mappát is létrehozni a szekcióhoz kapcsolódó képek, CSS fájlok
számára. Szóval ezzel elérhetjük, hogy egy jól strukturált MVC modult/részalkalmazást különítsünk el.
Az itt létrehozott kódok nem kerülnek külön dll fájlba, továbbra is a fő projekt assembly fájljába lesznek
belefordítva. Viszont célszerű az Area nevének megfelelő névterek használata a kontrollerek és a
modellek osztályaihoz.
Ami különleges az a regisztrációs osztály. Ezt ki is emeltem ide, hogy látható legyen a célja, ami nem
más mint, hogy itt tudunk az Area alatt levő almodulnak egyedi konfigurációt, viselkedést adni:
Az új route bejegyzés létrehozza az Admin/ -al kezdődő URL path-t és az alá rendelt kontrollereket,
actionöket elérhetővé tevő definíciót. Két kérdés szokott felmerülni:
Az első kérdésre a válasz az, hogy valójában az area lehet egy teljesen másik projekt teljesen másik dll-
jében is. Ekkor ez végzi el a kezdeti inicializálást. Illetve tudatja az MVC-vel, hogy van area definiálva.
Igen, ennyi kell csak neki. De mielőtt továbbmennénk, szeretnék rámutatni, hogy ezzel a hívással egy
objektumot is át tudunk adni az Area-nak, ami a külső dll-el area esetén nagyon jól jöhet:
Tehát az area-nak nem muszáj a főprojektben benne lennie, lehet egy teljesen különálló modul is.
Ebben az esetben a külön MVC projekt külön dll-be kerül, aminek nem lesz tudomása az alkalmazás
környezetéről, a global.asax-ban definiált route, filter, bundling beállításairól, adatbázis kapcsolatáról,
stb. Ezen a módon egy referenciát lehet átadni a külön dll-ben üzemelő area modulnak.
Adjunk hozzá egy új "Felhasznalok" kontrollert és a hozzá tartozó Index.cshtml View-t, majd próbáljuk
meg elérni az /Admin/Felhasznalok URL-el. "Nálam működött", mondja az egyszeri programozó. Eddig
nincs is semmi különös, miért ne menne. Kontroller, action, View, route rendesen be van állítva.
Sok-sok oldallal ezelőtt felvetettem, hogy nem lehet egy MVC alkalmazáson
belül két azonos nevű kontroller. Még akkor sem, ha más névtérben
vannak. Ami a C# fordítót egyébként nem zavarja, hisz ezért vannak a
névterek. Azon az oldalon előrehivatkoztam ide, hogy az area-val mindezt
meg lehet csinálni. Akkor most hozzunk létre egy HomeController nevű
kontrollert és a hozzá tartozó View-t. Indítsuk az alkalmazást, és lássuk mi
lesz. Ha az projektbeállítások (Web fül) úgy vannak beállítva, hogy a kezdő
URL a projekt gyökere, azaz nincs beállítva semmi, akkor a /Home/Index
actionje lépne működésbe, de ehelyett ezt kapjuk:
11.2 Real world esetek - Az alkalmazás modularizálása. Az Area. 1-330
Nem mondtam volna igazat, és az area sem segít? Navigáljunk tovább az /Admin/Home/Index –oldalra.
Ez viszont működik. A megoldás ott van a hibaüzenetben, csak részben eltakarja a nyíl. Az
alapalkalmazás Default route-jának fogalma sincs, mit csináljon a sok Home névvel, ezért specifikálni
kell, hogy mégis melyik névtérben tud Default-ként viselkedni. A megoldás egyébként működik normál
route bejegyzésekkel area nélkül is. Az alapalkalmazás kontrollereinek névtere:
namespace MvcApplication1.Controllers
namespace MvcApplication1.Areas.Admin.Controllers
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
namespaces: new string[] { "MvcApplication1.Controllers" }
);
Vastagon kiemeltem az új beállításokat. Nem is baj, hogy ott a LocalizedRoute is, mert így el tudom
mondani, hogy a DataTokens gyűjtemény feltöltése és az egy "Namespaces" elemének a hozzáadása
valamint a MapRoute "namespaces:" paramétere egy és ugyanaz a dolog, csak két különböző
megoldással. A lényeg, hogy beállítunk egy string tömböt a kapcsolt névtér/névterek felsorolásával.
A DataTokens-ek között még az area nevét, mint érdemleges beállítást értelmez az MVC. Ezért van az
area regisztrációban az alábbi felülbírálás:
Ebből lesz egy "area" = "Admin" data token. Visszatérve a route mappelések pontosításához,
hasonlóan érdemes beállítani az area regisztrációnál található route definíciót is arra a névtérre, ami
az area kontrollereinek a névtere lesz:
Talán mondanom sem kell, hogy mindez azért ilyen "nem egyszerű", hogy használhassunk több
HomeController-t. Ha erről a luxusról lemondunk, akkor a fenti route specifikálásra sincs szükség. Az
egésznek persze nem is az a lényege, hogy legyen öt HomeController-ünk, hanem az, hogy egy külön
projektben felépített area modulban ne legyen megkötve a kezünk. Képzeljük hozzá, hogy az area-t
tartalmazó modult egy 1000km-re levő, teljesen más csapat fejleszti. Ekkor jól jön, ha a fő modul és
az area-t tartalmazó modul a lehető legkisebb mértékben függ egymástól. Fontos, hogy jól
elszeparálható legyen olyan szinten is, mint a kontrollerek névkonvenciója. A további példákhoz egy
bevált további módosítást végeztem az area route regisztrációjában:
Ezzel az /Admin URL-hez is hozzá lesz társítva alapértelmezett kontroller. Illetve az area neve egy
konstansból jön, így könnyen módosíthatjuk az area elnevezését.
Ide tartozik, hogy amikor a könyv elején néztük, hogyha nem határozzuk meg egzakt módon az
action ViewResult vagy PartialViewResult visszatérési csomagjában a View relatív elérési útját, akkor
beindul a View keresgélős játék. Ennek során megnézi a kontroller nevének megfelelő mappát a
Views mappában, ha nem találja, megnézi a Views/Shared mappát is. Most hogy beindítottuk az
area-t, a játékba beszáll még ez is, mint szereplő. Így a keresés azzal fog kezdődni, hogy az adott area
Views/Kontrollernév mappájában fog nézelődni először. Utána az area Views/Shared jön, és csak ez
után jön az normál jól megszokott alapalkalmazásbeli Views mappák felkeresése. Erre érdemes
figyelni, mert azonos nevű kontrollerek esetén meg fogja találni az alap projekt View fájlját is, ha
elfelejtettük volna hozzá létrehozni a View fájlt az area Views mappájában.
Az area-ban levő linkgenerálást végző Html és Url helperekkel tudatni kell, hogy a link a fő alkalmazás
actionjére vagy az area-ban található actionre vonatkoznak. Az alábbi példa az area-ban levő
Home/Index útvonalnak megfelelő View részlete.
Az 1. link esetében ki kellett írni a route paraméter definícióját biztosító anonymous osztályban, hogy
a link ne az aktuális area beszámításával legyen létrehozva. Az area="" azt jeleni, hogy ne az area
route definíciói között keresse a link generálásához használandó bejegyzést.
A 3. link az area-n belüli Felhasznalok kontroller Index action-jére hivatkozik, de megadtam egzakt
módon, hogy az area-ből keresse ki. Itt jön jól az area név konstans.
Az alapprojektből nézve viszont pont fordítva kell csinálni: csak akkor kell meghatározni az area route
értéket, ha az area-ban levő action-re hivatkozunk. Az alábbi példa az alap projekt
Home/Index.cshtml fájljából éri el az area és az alapprojekt actionjeit:
Felmerülhet az igény, hogy az area-n belüli oldalak egységes kinézetűek legyenek, de ezen felül
legyen egységes a fő ág kinézetével is. A 6.5 fejezetben a Layout tárgyalásánál bemutattam, hogy a
layout-okat is lehet láncolni. Ez a módszer alkalmazható az alap projekt-ben levő _layout.cshtml és az
area-ban levő _layout.cshtml között is. Természetesen ilyenkor nagyon körültekintően kell
megtervezni a layout-ok hierarchiáját. Egy bevált módszer, hogy az alap projektben definiálunk egy
minimalista layout-ot, és ehhez láncolunk a fő projektben egy további layout definíciót, amit majd a
fő projektbeli View-k fognak használni. Az area-kban is egy-egy további leszármazottat lehet készíteni
az area-ra jellemző formai igények szerint. Esetleg még egy "areabaselayout" –ot is közbe lehet vetni,
de erre a legtöbb esetben nincs igazán szükség.
Fő projekt _layout
Az is lehetséges, hogy az area-ban levő View-k teljesen saját _Layout fájlt használjanak, mivel a
Layout felülbírálható a View-ban vagy az ActionResult-ban is. Sőt működik a közösített Layout
definíció is, ha az area View-s mappájában készítünk egy új _ViewStart.cshtml fájlt.
@{
Layout = "~/Areas/Admin/Views/Shared/_Layout.cshtml";
}
11.3 Real world esetek - Mobil nézetek, View variánsok 1-333
Mikor webfejlesztéssel kezdtem foglalkozni, még az volt a kérdés, hogy 800x600 vagy 1024x768
képernyőméretre optimalizáljuk a weblapokat, illetve hogy domináns Internet Explorer mellett vajon
fogják-e egyáltalán más böngészőben is használni az webalkalmazást. Most ilyen kérdés szóba se jöhet,
amikor a megjelenítőknek és a böngészőknek ilyen variánsa van használatban. Talán két éve még
mindig volt egy olyan választóvonal a mobil és a desktop célzatú webalkalmazás fejlesztések között,
ami abból a helyzetből adódott, hogy általában "a mobil eszköz" egy kisfelbontású, kis kapacitású
készülék volt. Szemben a normál nagysebességű nagyfelbontású számítógépekkel. Ma már ez sem
mondható el, ha egy tabletre gondolunk. Viszont a továbbiakban - követve a tradicionális gondolkodást
- "mobilnak" fogom nevezni a kis képességű kliens eszközöket. Olyan helyzetek kezeléséről szeretnék
részleteket mutatni, amikor a kinézetet, a generált HTML markupot, az elrendezést attól tesszük
függővé, hogy a kliens milyen képességekkel bír. A példakódokat a MobileTestController vezérli.
Az MVC alapon ezt jól meg lehet oldani egy külön area-val, amit a mobil eszközök számára
tartunk fenn. Persze ennek nagy ára van, mert nagyon sok mindent duplikálni kell.
A másik, amikor nincs site szeparáció. Csak egy közös oldalgeneráló, megjelenítési réteg van.
Ekkor a megjelenítési képességek szerinti elválasztást JS, CSS (media query) és HTML
lehetőségekkel, trükkökkel oldják meg. Ebben az esetben a felület érzékeny lesz a képernyő
felbontásra, képarányra, stb. Ezt "responsibe web design"-ként szokták emlegetni. Mivel ez
leginkább HTML és CSS alapú megoldás, az MVC nem tud előre elkészített, általános segítséget
adni ennek megvalósításához. Az Internet projekt template által készített alkalmazás is
részben ilyen. Ha megpróbáljuk a böngészőablak szélességét csökkenteni és elérjük a 850 pixel
szélességet, az oldal elrendezése átvált, igazodva a keskenyebb szélességhez. (@media only
screen and (max-width: 850px) { CSS szekció miatt)
A harmadik lehetőség pedig, amikor nincs szeparáció oldalszekció és URL szinten, hanem
egyedi sablonok vannak a különböző mobil és nem mobil eszközökre. Ebben az esetben, ha a
request egy normál eszközről érkezik a normál View generálja az oldalt, míg ha egy mobil
kütyüről jön a kérés, akkor egy a normálhoz tartalmilag hasonló, de egyedi View lesz az oldal
template fájlja. Ezt jól támogatja az MVC keretrendszer, ahogy láttuk is a 6.1 fejezetben.
Természetesen ezek csak fő csapásirányok, simán felhasználhatóak keverve, úgy hogy a számunkra
előnyös részeit használjuk ki mindegyiknek. A megfontolás tárgya lehet például, ha egy olyan oldalt
szeretnénk készíteni, ami tőzsdei információkat jelenít meg. Mivel egy pénzügyi oldalnál kritikus, hogy
mit és hogyan jelenítünk meg, milyen legyen a felület kezelése, elképzelhető hogy a teljes site
szeparáció is szóba jöhet. Míg, ha egy blogmotort készítünk, ahol a fő témán kívül hanyagolható
oldalrészletek is vannak (archív oldalak időrendi listája), egy responsible dizájn is megfelelő lehet.
11.3 Real world esetek - Mobil nézetek, View variánsok 1-334
A megoldások első közös pontja, hogy hogyan határozzuk meg azt, hogy mik a böngésző képességei.
Ezzel érintőlegesen foglakoztunk az egyedi action filereknél (9-242. oldal), mikor böngészőtípus-
függővé tettük az action elérését. Majd a speciális output cache esetében (10-302. oldal), ahol
böngészőnként különítettük el az oldalvariánsokat. Jelen helyzetben lehet, hogy ez utóbbi
nélkülözhetetlen lesz, ha gyorsítótárazni szeretnénk az oldalaink HTML eredményét
eszköztípusonként.
Az első action csak a teszt View-t szolgálja ki. A ChangeBrowserMode felülbírálja a böngésződetektálást
a bejövő 'mobile' paramétere szerint. Törölni kell a felülbírálást, ha a Requestben detektált
IsMobileDevice megegyezik azzal, amit szeretnénk. Ha át akarunk váltani a valóságosról a felülbíráltra,
akkor jön a SetOverriddenBrowser. Ez egy enum-ot vár, aminek a két értéke egy-egy User-Agent
stringet jelent. Ezek közül a kiválasztott stringgel fogja felülbírálni a böngészőből jövő, a HTTP header-
ben levő User-Agent értékét. Emiatt böngésző környezeteket szimulál:
BrowserOverride.Desktop – estén egy Windows XP alatt futó 6.1-es Internet Explorert jelent.
BrowserOverride.Mobile – Egy régi Windows Phone alatt futó 6.0 IE-t fog szimulálni.
Ez a beállítás elmentésre kerül egy '.ASPXBrowserOverride' nevű, 7 napos lejáratú cookie-ban, aminek
a tartalma a szimulált browser User-Agent értéke lesz. Hogyha a szimulált eszközt jobban szeretnénk
specializálni több és újabb böngészőre is, akkor HttpContext.SetOverriddenBrowser("your-user-
agent-text") metódusváltozattal tudjuk ezt megtenni.
11.3 Real world esetek - Mobil nézetek, View variánsok 1-335
Windows Phone Emulator 56 A komplett Windows Phone SDK részeként érhető el. Ingyenes.
MobiOne Studio 57 iPhone, iPad, Nexsus, Android precíz emulációk. 15 napos
próbaverzió.
Opera Mobile Emulator 58 Csak az Opera Mobile böngészőt emulálja különböző képességű
eszközök jellemzői szerint. Ingyenes.
Ezen kívül a böngészőben át lehet állítani a kiküldendő User-Agent szöveges adatait is. Chrome
böngészőben a "Developer Tools" jobb alsó sarkában a fogaskerék ikonnal lehet előhozni az
"Overrides" ablakot. Ebben előre elkészített User-Agent-ek közül választhatunk és még az eszköz
képernyőfelbontását is szimulálni tudjuk a Device metrics beállításával. Ugyanilyen lehetőség elérhető
egy kiegészítővel a FireFox-ban is.
View variánsok
Most térjünk át a mobil-desktop View változatok kezelésére. Láttuk, hogy lehet egy '.Mobile' utótaggal
jelezni a View fájl keresőjének, hogy mobil eszköz esetén azt a fájlt használja. Ez a módszer működik
bármilyen View értelmű fájl esetén is.
IndexPartial IndexPartial.Mobile
<h3>Desktop partial View</h3> <h3>Mobile partial View</h3>
@Html.Partial("IndexPartial")
<br />Nem szükséges így hivatkozni:<br />
@Html.Partial("IndexPartial.Mobile")
56
http://dev.windowsphone.com/en-us/downloadsdk
57
http://www.genuitec.com/mobile/
58
http://www.opera.com/hu/developer/mobile-emulator
11.3 Real world esetek - Mobil nézetek, View variánsok 1-336
Ez a logika működni fog a Layout fájlal is, de itt és a többi View variáns
esetén is az a szabály, hogy a variánsok egy mappán belül legyenek. Tehát
az nem működik, hogy a desktop _Layout.cshtml a Views/Shared alatt a
_Layout.Mobile.cshtml pedig máshol van. Viszont a _Layout-ok láncolhatóak, ahogy már láttuk, és
ezzel számos tervezési dilemmát fel lehet oldani.
Az eredeti megvalósítás csak annyit tesz annak felderítésére, hogy mobil eszközről van-e szó vagy nem,
hogy megnézi a felülbírált Browser beállítás IsMobileDevice tulajdonságát.
new DefaultDisplayMode("Mobile")
{
ContextCondition = context => context.GetOverriddenBrowser().IsMobileDevice
}
Tehát annak eldöntésére, hogy az MVC használja-e a definiált szöveges utótagot, csak a bool
visszatérésű metódust kell értelmesen megírni. Csak arra figyeljünk - a manuális User-Agent
felülbírálási lehetőség miatt - hogy a vizsgálathoz mi is a fenti GetOverriddenBrowser()- től kérjük el
a Browser objektumot, vagy a GetOverriddenUserAgent()- től a szöveges User-Agent-et.
11.3 Real world esetek - Mobil nézetek, View variánsok 1-337
Az eszközök sokkal precízebb felderítéséhez elérhető egy "51Degrees.mobi" nevű NuGet csomag,
aminek van egy fizetős, okosabb, naprakész változata is.
Láttuk, hogy nem csak a mobil eszközök számára lehet elkülönített, "dedikált" View-t készíteni, hanem
ha akarjuk akár a böngésző neve szerint is. A lehetőségek ezzel még nem zárultak le. A View
kiválasztását teljesen az irányításunk alá vonhatjuk. A RazorViewEngine osztály FindView
metódusának a felülbírálásával onnan töltjük be a .cshtml fájlt ahonnan akarjuk. Teljesen más
mappastruktúrából, de akár adatbázisból is. A 6-123 oldalon a ConciseViewEngine nevű osztállyal
csináltunk egy felülbírálást. Akkor a szükségtelen View fájlnév mintákat vettük ki, hogy ne keresgéljen
feleslegesen, de ugyan ebben az osztályban azt is megtehetjük, hogy a View betöltési logikát átírjuk a
FindView metódusban.
11.4 Real world esetek - Saját Html helperek, modell metaadatok 1-338
Miután belejövünk abba, hogy a View-t a Partial View-t a Display- és EditTemplate-eket rutinosan
használjuk, érdemes továbbgondolni, hogy vajon minden egyes esetben kell írni egy action-partial
View párost? Minek használjak View-t és razor kódot, mikor tudom, hogy ezt kóddá fogja fordítani az
MVC motorja, ami idő- és erőforrás-veszteség? Miért nem írjuk meg egyből kódszinten úgy, mint
akármelyik másik technológiában a kontrolokat (WinForms, Web Forms, WFP)? Álljunk a sarkunkra és
vegyük kezünkbe az irányítást és a HTML generálást!
Láttunk egy példát korábban, amikor egy meglévő Html helper által használt háttér metódust
használtunk. Most, próbaképpen legyen az a cél, hogy a Html5-ben megjelent textbox placeholdert
előnyeit ki tudjuk használni. Ennek a placeholder-nek az a célja, hogy az üres textbox szöveges részén
szürkével kiírja, hogy milyen adatot, esetleg milyen formátumban kell megadni. Ez helyettesítheti a
textbox előtt levő <label for=""> feliratot.
A célt úgy fogjuk elérni, hogy a property Display attribútumnak a mezőfeliratra, a label-re vonatkozó
szövege lesz a placeholder értéke is egyben.
A próbához kell egy action, mint mindig. A lényege, hogy a FullName-t üresre állítsa, hogy
megjelenhessen a szürkített szöveg:
Az új helper első változata ebben az esetben nagyon egyszerű, mert minden szükséges adat
megszerezhető egyetlen sorban:
Azt az igényt, hogy egy textbox jöjjön létre, a gyári TextBoxFor hívásával oldjuk meg. Ennek
továbbdobjuk az élő htmlHelper példányt és az expression-t. Úgy használjuk fel, mint egy normál
statikus metódust. A htmlAttribues paraméterében pedig átadjuk a 'placeholder'-t, amiből a HTML
attribútum lesz.
Az új dolog a ModelMetadata, ami egy nagyon okos herkentyű. Nagy vonalakban az a szerepe, hogy a
használt modelljeink típusáról, propertyjeiről, attribútumairól egy cache-elt adathalmazt tart fenn.
Ennek a cache-elésnek köszönhető, hogy a View-kban levő sok-sok lambda expression kiértékelésének
a sebessége elég gyors. Csak érdekességképpen megjegyzem, hogy az a része, ami felderíti és
szolgáltatja a modell típusinformációit, szintén lecserélhető. Emiatt lehetséges olyan ModelMetadata
providert készíteni, ami nem a modellről, hanem teljesen máshonnan olvassa fel azokat a definíciókat,
amiket az attribútumokkal szoktuk leírni. Ilyen forrás lehet egy XML fájl vagy az adatbázis is. Ezzel a
lehetőséggel el lehet érni, hogy nem kell a modell metainformációit a modellhez kötni fordítás időben.
Még a "buddy class" nevű lehetőséget sem kell használni ilyenkor. Ez CMS jellegű fejlesztésnél jól jöhet.
Mivel a ModelMetadata példány a Html helper fejlesztések kulcsa, érdemes egy kicsit boncolgatni,
hogy mik érhetők el ezen keresztül.
Típusinformációk és értékek:
11.4 Real world esetek - Saját Html helperek, modell metaadatok 1-340
A fentieken kívül még számos további képességet is rejt, amit a model binder és a validáció használ ki.
A metadata nem csak egy propertyről tud tájékoztatást adni, hanem a modellről is. Ilyenkor a
PropertyName üres és a ContainerType csak null.
[AdditionalMetadata("placeholder","Mi a neved?")]
public string FullName { get; set; }
Ez egy szöveges nevet és egy objektumot vár, ami a fenti példában megint csak szöveg. A következő
Html helper változatban az attribútum alapján létrejött "placeholder" indexű elemet vesszük ki a
ModelMetada.AdditionalValues gyűjteményből, és használjuk fel a placeholder értékeként. Az
AdditionalMetadata attribútum két paramétere a modellfelderítés során, kulcs-érték párként kerül az
AddtionalValues gyűjteménybe.
Ott van azért a DisplayName elérése is, mint szövegforrás, ha nem lett volna definiálva 'placeholder'
bejegyzés. Egy másik felhasználása ennek az attribútumnak, ha a modellosztályon használjuk:
Ezt pedig át tudjuk venni a View-n vagy egy Partial View-n belül:
{
@ViewData.ModelMetadata.AdditionalValues["modell metainfo"]
}
Elég nagy könnyebbség volt, hogy fel tudtuk használni a meglévő TextBoxFor statikus Html helper
extension metódust. Most ezen túllépve lemegyünk arra a szintre, ami a fejezet célja is volt, hogy elemi
HTML markupot építsünk kódból. A következő példa még mindig az előző placeholder-rel rendelkező
<input> mezőgenerálásnál marad, de ez alapján biztos vagyok benne, hogy bármilyen extrém HTML
markupot is össze fog tudni állítani az olvasó.
Az első lépés, hogy készítsünk egy olyan Html helper metódusváltozatot, ami csak a lambda expression-
t várja. Majd ahogy lenni szokott, következzen a sokparaméteres változat. Amikor újrafelhasználható
helpereket készítünk célszerű megtartani azt az elvet, hogy több túlterhelt metódusváltozatot
készítünk. Ezzel a módszerrel tudjuk biztosítani azt, hogy a View-ban - a felhasználás helyén - sokkal
rövidebb definíciókat kell írni, és elkerülhetjük a felesleges null-ok kiírogatását. Vagy lehet használni
opcionális paramétereket is, amikor a paramétereknek alapértelmezett értéket adunk a
metódusdefinícióban. Egyparaméteres változat, ami csak továbbhívja a sokparaméteres verziót:
Sokparaméteres változat.
//Szöveg formázása
string valueParameter = htmlHelper.FormatValue(metadata.Model, format);
string attemptedValue = null;
//HTML generálása
return new MvcHtmlString(tagBuilder.ToString(TagRenderMode.SelfClosing));
}
A példakódot a kommentek elég jól megmagyarázzák, de azért nézzük végéig mi-miért van ott. Az első
lépésben természetesen a nélkülözhetetlen ModelMetadata megszerzése történik. A 'Property path'
szerepét a model binder-nél láttuk: az egymásba ágyazott View-k és editor template-ek
hierarchiájában a property neveket ponttal elválasztva kell összefűzni. Ezt tudja szolgáltatni
GetFullHtmlFieldName. (propertyA.propertyB[0].propertyC))
A következő lépésben indul a Html elem építése. A TagBuilder egy elmés szerkezet. Meg kell neki adni,
hogy milyen Html taget akarunk felépíteni, majd az attribútumait, CSS osztályait bele kell tölteni a belső
gyűjteményeibe. Amikor ezzel végeztünk csak meg kell hívni a ToString-et és kidobja a
felparaméterezett HTML elemet. A ToString rendelkezik egy renderelési mód paraméterrel. Itt most az
lett neki mondva, hogy önlezáró taget készítsen, mert az <input /> is ilyen, és nem <input><input/>
formátumú.
A metainformációk alapján a szöveg formázása egy lényeges pont, mert itt történik meg a
kúltúrainformációk alapján a szöveggé alakítás, ha a property típusa nem string. Dátumformázás,
pénznem stb.
Azt is fontos szem előtt tartani, hogy egy Html helpernek tudnia kell valamit kezdeni a validációs
hibákkal. Amikor a felhasználó nem megfelelő értékkel küldi a formot, a validáció során az általa
megadott értéket célszerű visszaküldeni és nem a modellben tárolt kezdeti adattal feltölteni. Például,
ha valamit elírt a textbox-ban, akkor lássa a hibaüzenetet és a hibás adatot egyszerre. Erre szolgál az
attemptedValue változó és felhasználása.
Névterek
A beépített Html helperek névtere a System.Web.Mvc.Html. Amikor saját Html extension metódusokat
kezdünk el gyártani, felvetődhet a kérdés hogy milyen névtérbe tegyük azokat? Ha csak a saját
projektünkben használjuk, akkor nagyjából mindegy is, de ha máshol is hasznosítani szeretnénk,
célszerű valamilyen "gyártó"/cégnév jellegű és/vagy funkcionálisan csoportosító célzatú névtérbe
tenni. A legelső kézenfekvő megoldás, hogy a saját helpereket szintén a System.Web.Mvc.Html-be
tesszük, de ez nem javasolt. Annyi előnye azért van, hogy a helperek azonnal használhatóak lesznek a
View-ban, mert az MVC tud erről a névtérről a Views/web.config beállítása alapján:
<system.web.webPages.razor>
<pages pageBaseType="System.Web.Mvc.WebViewPage">
<namespaces>
<add namespace="System.Web.Mvc" />
<add namespace="System.Web.Mvc.Ajax" />
<add namespace="System.Web.Mvc.Html" />
<add namespace="System.Web.Optimization"/>
<add namespace="System.Web.Routing" />
</namespaces>
</pages>
</system.web.webPages.razor>
11.5 Real world esetek - Fájl le- és feltöltés 1-343
Abban az esetben, ha ajánlottan saját névteret használunk, akkor vagy feltüntetjük a View elején egy
@using –al, vagy új elemet adunk a fenti web.config 'namespaces' definíciólistához.
A weben keresztüli fájlkezelés egy régi problémakör, és valószínű, hogy minden webfejlesztőnek dolga
akad vele. Egy kis elmélkedéssel kell kezdenem a fájlfeltöltésről, hogy rá tudjak világítani néhány
sarkalatos pontra.
A gond már ott elkezdődik, hogy az egész internetet még mindig átszövi az a koncepció, mintha
a fájlokat csak kiszolgálni kellene a szervereknek. A fájl le és feltöltés megvalósíthatóságainak
egyik fő kerékkötője még mindig az ISP-k aszimmetrikus sebességű szolgáltatása. Ez a fájlok
feltöltése szempontjából azt okozza, hogy a feltöltés nagyon hosszú ideig eltarthat. Egy átlag
letöltéshez képest 10-100x különbség is lehet. Ne feledjük el, hogy nem a szerver sávszélessége
a döntő, hanem az is lehetséges hogy lassú mobilinternet kapcsolatról fognak feltölteni. Mivel
hosszú a műveleti idő, arányosan nagyobb a valószínűsége, hogy megszakad a kapcsolat és
érvénytelen lesz a fájlfeltöltés. Mivel a feltöltés sikeressége rendkívül bizonytalan, célszerű a
feltöltést ideiglenes fájlba irányítani és nem a végleges helyére.
A "minden input az ördögtől való" és emiatt nagyítóval kell megvizsgálni a felhasználótól
érkező adatokat elve a fájlfeltöltésnél hatványozottan igaz. Néhány szempontot ajánlanék a
vizsgálódáshoz:
o Ne fogadjunk fájlt hitelesítés nélkül. A fájl érték, és lehetnek jogi vonatkozásai is.
Naplózzunk minden műveletet.
o Vizsgáljuk meg, hogy a fájl tartalma, a kiterjesztése, és amit a felhasználó róla állított
(mondjuk, hogy fotó) az valóban úgy van-e. Nagyon sok problémát okoztak már a
fájlnév által állított és a valódi tartalom közti ellentétek. (Vírusok, a felhasználók
megvezetése, stb.). Eleve ne engedjünk meg mindenféle fájlkiterjesztés és fájltartalom
használatát az alkalmazásunkban.
o Szabjunk határokat a fájl méretére, és a le- és feltöltési idejére. A túl kicsi és a túl nagy
fájl is problémás lehet. Mindkettő lehet támadási forma is. A túl naggyal teletölthetik
a fájlrendszert. A túl kicsivel is gond van, ha nagyon sokat küldenek fel. Egyrészt a
validációra sok erőforrás rámegy. Másrészt kérdéses hogyan kezeljük le, ha egy
célmappába 20-50 ezer fájlt másolnak fel. Ezt a legtöbb operációs rendszer nem
szereti. Szintén érdemes meggondolni, hogy napi/órai limitet vezessünk be
kombinálva a mérethatárokkal. Az IIS szervernek meg lehet mondani a fájlméret- és az
időkorlátot is. Majd be is fogjuk állítani, mert az alapértelmezett határok elég
szűkösek. Az időperiódus alapú limitet viszont nekünk kell megoldanunk, ha
szükségessé válik.
A mappa jogosultságok. Valahova fel kell tölteni a fájlt, de az ilyen helyet nagyon bástyázzuk
körül. Ne lehessen onnan fájlt végrehajtani, csak a mi alkalmazásunk írhassa. Szigeteljük el,
amennyire csak lehet. A felhasználóink érdekében, a feltöltött fájlok közvetlen URL-erőforrás
mapping alapú letölthetőségét is korlátozzuk, mondanám: ne engedjük. Az ilyen jellegű URL
alapú közvetlen elérésre gondolok: www.security1988kft.hu/upload/stuff/eztnézdmeg.exe.
A fájlnév probléma. Régebben ez igen kritikus volt az ékezetes nevek miatt. Ma már jobb a
helyzet az UTF8 karakterkódolású fájlnevek miatt. Azonban a fájlnév hossza még mindig egy
11.5 Real world esetek - Fájl le- és feltöltés 1-344
Kezdjük az egyszerű lépésekkel. Hozzunk létre egy Upload mappát a projekten belül, ahová majd
mentjük a fájlokat. Ezt éles helyzetben írhatóvá kell tenni az alkalmazást futtató IIS felhasználó
számára. Akinek a nevében fut az alkalmazásunk pool-ja.
Szükség lesz egy multipart formra egy View-ban, a file típusú feltöltési input taggel:
[HttpPost]
public ActionResult Upload(HttpPostedFileBase uploadedfile)
{
if (uploadedfile != null && uploadedfile.ContentLength > 0)
{
if(uploadedfile.ContentType != "image/png")
return new ContentResult() {Content = "Csak PNG fájlt tölthetsz fel!"};
A post action paraméter HttpPostedFileBase típusát ismeri a model binder és szépen fel is tölti, mivel
az input mező azonos nevű (name="uploadedfile"). Nincs is más dolgunk csak megadni a fájlrendszeren
belüli elérési utat és a SaveAs metódussal elmenteni a fájlt. A Server.MapPath metódusa a kapott
11.5 Real world esetek - Fájl le- és feltöltés 1-345
alkalmazáson belüli relatív útvonalból abszolút fájlelérési utat készít. Persze némi validációt célszerű
megtenni. Erre szolgál az a sor, hogy legalább a nulla hosszú fájlokat ne mentsük el. Ezen kívül a
ContentType tulajdonságán keresztül megkapjuk a fájl MIME típusnevét. Ezt is megvizsgáljuk, és csak
PNG fájlokat engedünk feltölteni a példában.
Több fájl feltöltés lehetőségéhez, két úton is eljuthatunk. A régi módszer, hogy felsoroljuk a file input-
okat. Ebben az esetben, a model binder miatt az indexelt végű elnevezést kell használni
(uploadedfile[x]). A HTML 5-ös módszer a 'multiple' attribútummal kiegészített fájl input mező az újabb
lehetőség. Ezzel elég csak a normál nevet megadni.
A feldolgozó actionben csak annyi a dolgunk, hogy az IEnumerable képes paraméterrel várjuk a
fájlokat.
[HttpPost]
public ActionResult UploadMulti(HttpPostedFileBase[] uploadedfile)
{
foreach (var file in uploadedfile)
{
if (file != null && file.ContentLength > 0)
{
var fileName = Path.GetFileName(file.FileName);
var path = Path.Combine(Server.MapPath("~/Upload"), fileName);
file.SaveAs(path);
}
}
return RedirectToAction("Index");
}
Vajon a fenti kód nem száll el a foreach-nél, ha nem jelölünk ki fájlt a formon? Érdekesség, hogy ha
nem jelölt ki a felhasználó fájlokat és úgy küldi be a formot, akkor sem lesz null az uploadedfile
paraméter tartalma, hanem egy elemű felsorolás lesz benne, aminek az értéke null.
A feltölthető fájl méretét nem, csak a request tartalmának a teljes méretét tudjuk korlátozni. Erre
szolgál a web.config-ban a MaxRequestLength értéke, ami kilobájtokban értendő. Alapértelmezetten
4MB a határ (4096).
<system.web>
<httpRuntime maxRequestLength="16384" requestLengthDiskThreshold="128" executionTimeout="600"/>
</system.web>
11.5 Real world esetek - Fájl le- és feltöltés 1-346
Mind a három paraméter a teljes requestet szabályozza, ebbe beleértendő az összes fájl, amit egy
menetben töltünk fel.
Most nézzünk meg egy összetettebb példát, ami ezeket a célokat valósítja meg:
Legyen egy fájlfeltöltési lehetőségünk. Modell szinten szabhassuk meg a feltöltendő fájl
maximális méretét, a fájl típusát, és azt hogy egyszerre egy vagy több fájlt lehet feltölteni.
Kliens oldalon is tudjuk korlátozni, hogy milyen fájlokat lehet kiválasztani a feltöltéshez.
A feltöltés utáni lépésben jelenjenek meg a feltöltött fájlok egy táblázatban és a felhasználónak
ki kelljen töltenie a fájl leírását. (mivel a fájl neve sokszor nem beszédes). A leírást
mindenképpen ki kell hogy töltse a felhasználó. Amelyik fájlnál kitölti, az elmentére kerül,
amelyikekhez nem ad meg leírást, azokból egy új táblázat képződjön és kérje be a hiányzó
leírásokat.
A file upload input mezőt Html helper állítsa elő, paraméterezve a feltölthető fájltípusokat.
(accept attribútum)
A feltöltött fájlokat Guid fájlnévvel tárolja el. A feltöltött fájlokat egy táblázatból kiválasztva le
is lehessen tölteni.
A példa során megnézzük, hogyan lehet (érdemes) egyedi, property szintű .Net attribútumot
kiértékelni. Azt is, hogyan lehet a modell validációját hol így, hol úgy, kiértékelni. A ModelState
összesített validációját megkerüljük és egyedileg nézzük meg, hogy az adott propertyhez megadott
értéket elfogadjuk vagy nem. Annyit előrebocsájtok, hogy a fenti célokat legalább 4-5 különböző
megközelítéssel is el lehetne érni. Ahogy látható ez részben folytatása is lesz az előző Html helperekről
szóló fejezetnek, és további példákat szolgál a validáció és a ModelMetadata kapcsolatára is. A példák
kódjai a Controller/File mappában vannak.
Legelőször a model:
//Fájl feltöltője
public string UserId { get; set; }
[Display(Name = "Fájlnév")]
11.5 Real world esetek - Fájl le- és feltöltés 1-347
[Display(Name = "Leírás")]
[Required]
public string Description { get; set; }
[Display(Name = "Fájlméret")]
public long Length { get; set; }
//feltöltési állapot
public UploadStatus Status { get; set; }
//Fájl típusa
[Display(Name = "MIME típus")]
public string MIME { get; set; }
A modell kétcélú. Első lépésben fogadja a post requestből a feltöltött fájlokat. Ezek a 'Files'
tulajdonságába kerülnek. Ezen a propertyn van egy egyedi FileUploadValidation validátor attribútum.
A modell követi, hogy az általa hordozott adatok a feltöltés mely lépésénél tartanak. Amolyan mini
workflow állapotot. Emiatt a modell állapota négy féle lehet: None - most érkezett a fájl a böngészőből,
Temp - Ideiglenesen eltárolva, de még nincs kitöltve a leírása, Uploaded - Leírás kitöltve fájl elmentve
a végső helyére, Deleting - A fájl törlésre kijelölve.
if(files == null)
{
//Valid, mert már fel lett töltve?
var dbFileModel = FileModel.GetById(filemodel.Id);
if (dbFileModel.Status == FileModel.UploadStatus.Temp)
return ValidationResult.Success;
return NoFiles();
}
Az IsValid metódusban dől el, hogy elfogadható-e a 'Files' property tartalma. Itt van két kiértékelési
irány:
Az MVC4-ben .Net 4.5 alatt vagy az MVC futures-t használva elérhető a FileExtensionsAttribute. Ezzel
a feltöltésre kerülő fájlok normál fájlkiterjesztése alapján lehet validálni. Tehát nem a MIME típusuk
alapján, mint amiről az előbb szó volt, és emiatt ez nem olyan jó lehetőség.
A következő szereplő a Html helper lesz, amivel előállítjuk az <input> mezőt. Ennek belső felépítése
már ismertős lehet az előző fejezetből:
Az első megoldás, amihez hasonló példa tucatnyi van a neten, úgy éri el a célját, hogy új
ModelMetadata providert használ fel. Az ilyen providerek feladata, hogy a modellről kinyerhető
információkat összegyűjtsék tulajdonságonként.
Mindössze a CreateMetadata metódust bírálja felül. Meghívja az ős azonos nevű metódusát, majd ha
a propertyn definiálva van a FileUploadValidation attribútum, akkor azt egy statikus string indexxel
(FileUploadValidationName) ellátva berakja az AdditionalValues gyűjteménybe. Ezt tudtuk elérni a
FileUploadFor Html helperből. Ennyi még nem elég, mert az MVC-nek meg kell mondani, hogy a fenti
providert használja. Megint csak a global.asax Application_Start-ja a célterület a következő sor
számára:
A másik megoldás - ami nem annyira populáris, mint amennyire jó - azt használja ki, hogy
implementálja az IMetadataAware interfészt a validációs attribútumon. Valójában az
AdditionalMetadataAttribute sem csinál mást, mint ezt az interfészt megvalósítja. Emiatt az első
megoldás providere és regisztrációja is kihagyható. Az újdonsült validációs attribútumunkat egészítsük
ki az IMetadataAware-el, és a megvalósítás nyúlfarknyi, egysoros kódjával. (Ezért mondtam, hogy
jobb.)
A View-kat nem másolom ide helytakarékossági okokból, teljes terjedelmükben, mivel elég egyszerűek.
A fenti action és View-jának eredménye:
Az 'Új feltöltés' link actionje biztosítja a lehetőséget az új fájlok feltöltésére. A képen az látható, hogy
megpróbáltam úgy feltölteni, hogy nem jelöltem ki fájlt.
[HttpPost]
public ActionResult UploadNew(FileModel model)
{
//Csak a 'Files' érdekes most.
if (!ModelState.IsValidField("Files"))
{
return View(model);
}
A post-ot feldolgozó action azzal indul, hogy csak a 'Files' property validációjára kíváncsi. Ha nem valid,
akkor visszadobja a View-t és megjelenik a jobb sarokban levő kép. Ha feltöltésre kerültek fájlok, akkor
azokból egyenként csinál egy-egy új FileModel-t. A modell státusza ekkor még 'Temp', ami jelzi, hogy
már fel van töltve, de még nincs kész. Az egyszerűség kedvvért a FileModel id-je és a feltöltött fájl neve
azonos Guid. A SaveAs-al elmentésre kerülnek a fájlok az Upload mappába. A sikeresen feltöltött
fájlokból álló lista szintén elmentésre kerül (AddRange). Ez lesz a fizikai fájlnevek és a feltöltött
fájlnevek közti mappelés, ez fog megjelenni a fájlok listájában. Utána a vezérlés az UploadFill actionhöz
kerül, ahol majd a felhasználó egyenként kitöltheti a fájl leírásait. Íme, a View egy darabja, hogy lássuk,
hogy az új Html helper metódusunk dolgozik:
[HttpPost]
public ActionResult UploadFill(List<FileModel> postedfileslist)
{
var newfiles = FileModel.GetList().Where(f => f.UserId == Session.SessionID
&& f.Status == FileModel.UploadStatus.Temp).ToList();
var remaininvalid = new List<FileModel>();
}
if (remaininvalid.Count == 0) return RedirectToAction("UploadList");
Valójában ez már nem kötődik annyira a fájlfeltöltés témaköréhez, viszont érdemes megnézni a
validáció csűrése-csavarása miatt. Mivel most semmi más nem számít csak a Description tulajdonság
kitöltése, ezért csak az ezekhez tartozó hibákat nézzük meg. Ha minden rendben van, akkor a status
Uploaded lesz, és a Description értéke tárolásra kerül. A FileModel szerkesztési lehetőségét a model
típusnevének megfelelő fájlnevű editor template biztosítja:
@model MvcApplication1.Controllers.Files.FileModel
<tr>
<td>
@Html.DisplayFor(model => model.FileName)
@Html.HiddenFor(model => model.Id)
</td>
<td>
@Html.EditorFor(model => model.Description)
@Html.ValidationMessageFor(model => model.Description)
</td>
<td>
@Html.DisplayFor(model => model.Length)
</td>
<td>
@Html.DisplayFor(model => model.MIME)
</td>
</tr>
Van egy kérdés a kódban (1. Miért van erre szükség). A probléma a következő, ha a felhasználó kitölti
az első sort (Borító kép), majd elmenti, akkor az egész validációs hibalista alapján kerül legenerálásra
a View. Mivel ennek a listának a 0. eleme éppen most valid és az 1. nem valid, a megjelenő View-ban
csak egy sor fog maradni (az mvc4-plus-qr fájllal). Viszont mivel a 0. elemen még ott van a kitöltött
'Borító kép' szöveg, annak tartalma kerülne a mvc4-plus-qr fájlhoz. Ebben a pillanatban már az a 0.
elem. Ezért kell újra összeállítani a ModelState belső listáját. Erre az egészre elvileg nem sok szükség
van, ha a kliens oldali validáció is működik, mert az egész táblázat nem kerül feltöltésre, amíg minden
sor nincs kitöltve:
@section Scripts {
@Scripts.Render("~/bundles/jqueryval")
}
11.5 Real world esetek - Fájl le- és feltöltés 1-355
Fájl letöltése
Ennek a fontos beállítását kiemeltem. Ha ezt nem állítjuk be, akkor (pl. képfájl esetén) a böngészőben
jelenik meg a fájl tartalma, és nem kínálja fel a böngésző a mentést.
A fájlok le- és feltöltésének témaköre ezzel még nem teljes. Gyakori felhasználói igény, hogy a fájl
küldés esetén egy progress bar jelezze a készültséget. Az is előfordulhat, hogy a fájl feltöltését AJAX-
osan szeretnénk megoldani 59 . Esetleg valami jól kézben tartott módon 60 . A fenti példában annyit
biztosítottunk a felhasználónak, hogy a feltöltött fájlokat leírással lássa el, amolyan meta
információként, de olyan igény is felmerülhet, amikor a fájlokat rendezni, kategorizálni szeretnék61.
59
http://balassygyorgy.wordpress.com/2013/04/17/ajaxos-fajl-feltoltes/
60
https://code.google.com/p/swfupload/
61
http://www.jquery4u.com/plugins/10-jquery-file-manager-plugins/
11.6 Real world esetek - Dolgozzunk egyedi View sablonokkal! 1-356
Ahogy eddig is láttuk, a Visual Studio megkönnyíti számunkra egy új kontroller vagy egy új View
létrehozását. View létrehozásához elég csak az action végén levő View metóduson az 'Add View…'
menüpontot használni. Az előugró dialógusablakban csak fel kell paraméterezni, hogy milyen View fájlt
szeretnénk létrehozni és az legyártja olyanra, amilyenre előírja egy sablon. De ez a sablon nagyjából
arra elég, hogy megbarátkozzunk az MVC használatával. Amikor rendes alkalmazást kezdünk építeni,
ezeket a View fájlokat rendszeresen és alaposan át kell írogatni. Hogy ezeket a felesleges köröket el
tudjuk kerülni, célszerű a beépített sablonokat lecserélni, bővíteni.
Ezekben a mappákban már konkrétan azokat a .tt kiterjesztésű T4 template fájlokat találjuk meg, ami
alapján a View-k és a kontrollerek elkészülnek. Ha nem találnák meg a fenti elérési utat, akkor érdemes
rákeresni a 'List.tt' nevű fájlra. Ebből több is lesz, de a mappanevek alapján biztos megtalálható, hogy
melyik mappáról is van szó.
A Visual Studio észreveszi a .t4 fájlokat és el is indítja a generálást azok alapján. Ez a viselkedés most
nem jó nekünk, mert ezeket a sablonokat manuálisan szeretnénk használni az 'Add View…' dialógus
62
Az gyári template-eket nem érdemes felülírni.
11.6 Real world esetek - Dolgozzunk egyedi View sablonokkal! 1-357
A fenti példa alapján megjelent a DemoEdit. A 'List' kiválasztásakor viszont már nem a VS beépített
sablonja, hanem a projektünkben található CodeTempates/AddView/CSHTML/List.tt sablon fog
működésbe lépni. Tehát a template is felülbírálható, amivel egyedi
fejlesztési szabványt tudunk létrehozni magunknak vagy az egész
team munkához, mivel ezek a .tt fájlok ugyanúgy feltölthetőek a
source control-ba.
A T4 sablonok megértése nem szokott nehézségeket okozni. Leginkább a Web Forms aspx fájl
logikájára hasonlít a felépítése és a szintaxisa. A statikus szövegekbe a <# #> tagek közé kell írni a C#
kódot. Ha dinamikus tartalmat szeretnénk az adott pozícióból kiírni, azt <#= #> közé kell írni.
Természetesen olyan kódot írhatunk ide, aminek string az eredménye. A fájl elején van néhány
direktíva, ami például a generálandó fájl kiterjesztését, a template-n belül használt nyelvet határozza
meg. Ezeket <#@ #> közé kell írni. A segédmetódusokat, amik közvetlenül nem vesznek részt a fájl
tartalmának előállításában a <#+ #> jelek között kell szerepeltetni. Egy rövid részletet emeltem ki, ami
az editor mezők generálásáért felel:
Remélem nem megtévesztő, de a '@Html.DropDownList(" ' itt most statikus szöveg, nem a T4
értelmezőnek szól, mivel az nem ismeri a razort. Egyik template nyelven állítjuk elő a másik template
nyelven írt template-et. Mivel a T4 tárgyalása nem ennek a könyvnek a témája itt le is zárom, de némi
ügyeskedés és kísérletezés biztos meghozza a gyümölcsét, ha ezeknek a fájloknak a módosítása a
feladat.
63
http://visualstudiogallery.msdn.microsoft.com/
11.6 MVC 5 újdonságai és változásai - Dolgozzunk egyedi View sablonokkal! 1-358
A korábbi fejezetekben már volt szó ezekről az újdonságokról a -al jelölve. Ez csak egy
összefoglaló. Sajnos az itt felsorolt újítások közül nem mind érhető el a VS2013 preview változatban.
Az abban levő System.Web.Mvc assembly 5.0-ás ugyan, de messze nem a végleges változat.
Támogatva van a HTML 5 color input beviteli mező, amivel a böngészőben, színválasztóval tudunk
kiválasztani egy színt. Az input mező value tartalma a normál HTML színmeghatározás szerinti
#RRGGBB formátumban lesz tárolva, de a modell propertyjében már System.Drawing.Color típus
szerint tudjuk kezelni. A DataType modellproperty attribútum
A Visual Studio 2013 preview alapján úgy tűnik, hogy az eddigi MVC Internet projekt template-et
lecserélik - a bootstrap által nyújtott előnyöket kihasználva – egy korszerűbbre. Az alkalmazás projekt
template-eknél maradva az MVC és a WebForms projektek is nagyon egyforma felépítésű projektet
generálnak. Sőt a két platform egy projektben is üzemelhet, mivel a routing, a friendly URL már az Web
Forms esetén is használható. Ezt egyébként eddig is meglehetett csinálni az MVC 4 alatt, csak nem volt
ennyire egyszerű.
Teljesítményjavítások
Az MVC motorjában számos olyan helyet azonosítottak, ami kihatással volt a teljesítményre. Ezek
rendszerint gyűjtemények feltöltésekor és kiértékelésekor jelentkeztek. Például a model binder
esetében. Ezeken a helyeken a LINQ formulákat normál for ciklusokkal helyettesítették. Mérési
eredményt még nem láttam, így nem tudok véleményt mondani, hogy ez egyes esetekben hány %-os
javulást okoz.
AcceptAttribute: DataTypeAttribute
A HTML5 rendelkezik a file feltöltésre egy type="file" input mező típussal. Ennek az accept attribútuma
tartalmazza a feltölthető fájltípusokat.
11.6 Utószó - Dolgozzunk egyedi View sablonokkal! 1-360
Utószó
Miután megismertem az MVC lelkivilágát egy olyan kép alakult ki bennem, hogy ez a keretrendszer
tulajdonképpen nem keretet ad valamire, bezárva a lehetőségeket, hanem inkább egy kelyhet vagy
egy foglalatot arra, hogy építkezzünk rá. Egy jól felépített MVC alapú projekt mérete sokszorosa az
MVC keretrendszer méretének. Ebben a képben az MVC olyan, mint egy nagy épület alapja, ami alig
látszik, mert benn van a földben. Akárhogy nézem egy elég jó alapot szolgáltat nagy alkalmazások
webes rétege számára.
Egyszer minden elér a végéhez. Egyszer le kell zárni a témát. Pedig lenne még miről írni… Ahogy
kezdtem a könyv elején: a téma nagyon szerteágazó. Sok-sok téma kimaradt, amiket az oldal aljáig
tudnék listázni. Majd gondolkodhatnék, hogy vajon mi maradt ki, mi maradt homályban, hol nem
rántottam le a leplet összefüggésről? Szóval soha sem lenne vége. A tervem szerint, ha bírom még
szabadidővel, és lesz is rá igény folytatni fogom.