Anda di halaman 1dari 360

Regius Kornél

(MVC 5 előzetessel)

Szöveg verzió: V1.0.0827


1-2

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.

A könyv és a kapcsolódó példaprogramok szabadon


felhasználhatóak a Creative Commons szellemisége
szerint. Az egyetlen kikötés, hogy nevezd meg a
szerzőt és a könyvet a fejezetcímmel együtt.
1-3

1. FELVEZETŐ ..................................................................................................................................... 1-6

1.1. AJÁNLÓ ................................................................................................................................................. 1-6


1.2. A SZERZŐRŐL .......................................................................................................................................... 1-7
1.3. HASZNOS DOLGOK ................................................................................................................................... 1-8
1.4. A RÖVIDÍTÉSEKRŐL, NEVEKRŐL ÉS JELEKRŐL................................................................................................... 1-9

2. BEVEZETÉS ................................................................................................................................... 2-10

2.1. A TENDENCIÁK ÁTTEKINTÉSE .................................................................................................................... 2-10


2.2. A WEBES ALKALMAZÁSOKRÓL ÁLTALÁBAN ................................................................................................... 2-11
2.3. BÖNGÉSZŐ – SZERVER INTERAKCIÓ ............................................................................................................ 2-12
2.4. AZ ELŐZMÉNY. AZ ASP.NET WEB FORMS ................................................................................................. 2-13
2.5. ASP.NET MVC PLATFORM ELŐNYEI ......................................................................................................... 2-15
2.6. AZ ASP.NET ÉS MVC FRAMEWORK ......................................................................................................... 2-16
2.1. AZ MVC KOMPONENSEI ÉS BESZERZÉSÜK. .................................................................................................. 2-16
2.2. A BÖNGÉSZŐKRŐL.................................................................................................................................. 2-17

3. ELSŐ MEGKÖZELÍTÉS .................................................................................................................... 3-19

3.1. AZ MVC ARCHITEKTÚRA ......................................................................................................................... 3-19


3.2. A MODELL ........................................................................................................................................... 3-21
3.3. A VIEW ............................................................................................................................................... 3-23
3.4. A KONTROLLER ..................................................................................................................................... 3-25
3.5. PRÓBÁLJUK KI! ...................................................................................................................................... 3-26
3.6. AZ ALKALMAZÁS FELÉPÍTÉSE ..................................................................................................................... 3-28
3.7. ÚJ MODEL, VIEW, CONTROLLER HOZZÁADÁSA ............................................................................................ 3-29
3.8. PRÓBÁLJUK KI MENTHETŐ ADATOKKAL! ...................................................................................................... 3-36
3.9. A PROJEKT BEÁLLÍTÁSAI ........................................................................................................................... 3-43
3.10. A MVC KOMPONENSEINEK MŰKÖDÉSI CIKLUSA ........................................................................................... 3-44

4. MODELL ....................................................................................................................................... 4-46

4.1. MODELL ÉS TARTALOM ........................................................................................................................... 4-46


4.2. MODELL ÉS KÓD .................................................................................................................................... 4-48
4.2.1. Viselkedéssel bővített modell ...................................................................................................... 4-48
4.2.2. Üzleti logikával bővített modell.................................................................................................... 4-50
4.2.3. A konstruktor probléma ............................................................................................................... 4-51
4.3. MODELL ÉS JELLEMZŐK ........................................................................................................................... 4-52
4.3.1. Megjelenés ................................................................................................................................... 4-52
4.3.2. Validáció attribútumokkal ............................................................................................................ 4-55
4.4. MODELL ÉS TÁROLÁS. ADATPERZISZTENCIA ................................................................................................. 4-63
4.4.1. Adatbázis séma szerinti modellek ................................................................................................ 4-64
4.4.2. Nézet jellegű modell..................................................................................................................... 4-66
4.5. AZ ÉRINTHETETLEN, GENERÁLT MODELL PROBLÉMÁJA ................................................................................... 4-67
4.6. EGYÉB MODELLATTRIBÚTUMOK ................................................................................................................ 4-68
4.7. EGY DEMÓ MODELL................................................................................................................................ 4-69

5. A KONTROLLER ÉS KÖRNYEZETE ................................................................................................... 5-71

5.1. AZ ALKALMAZÁSUNK BEÁLLÍTÁSA. A WEB.CONFIG ........................................................................................ 5-71


5.2. AZ ALKALMAZÁS KIINDULÁSI PONTJA. A GLOBAL.ASAX ................................................................................... 5-73
5.3. ROUTING ............................................................................................................................................. 5-77
5.4. CONTROLLER ........................................................................................................................................ 5-88
5.5. ACTION ÉS PARAMÉTEREI ........................................................................................................................ 5-95
5.6. AZ ACTION KIMENETE, A VIEW ADATOK ...................................................................................................... 5-99
5.7. ACTIONRESULT ................................................................................................................................... 5-101
1-4

5.8. ACTION KIVÁLASZTÁSA .......................................................................................................................... 5-108


5.9. FILTEREK ............................................................................................................................................ 5-110

6. A VIEW ....................................................................................................................................... 6-121

6.1. A VIEW MAPPÁK ................................................................................................................................. 6-121


6.2. A VIEW FÁJL KIVÁLASZTÁSA.................................................................................................................... 6-122
6.3. TARTALMA, TÍPUSOS VIEW .................................................................................................................... 6-125
6.4. PARTIAL VIEW..................................................................................................................................... 6-127
6.5. A VIEW-K EGYMÁSBA ÁGYAZÁSA ............................................................................................................. 6-133
6.6. A VIEW NYELVEZETE ............................................................................................................................. 6-139
6.6.1. Razor szintaxis ............................................................................................................................ 6-139
6.6.2. Kód a View-ban .......................................................................................................................... 6-143
6.6.3. Razor kulcsszavak ....................................................................................................................... 6-146
6.7. A VIEW KONTEXTUSA ........................................................................................................................... 6-150
6.8. BEÉPÍTETT HTML HELPEREK.................................................................................................................... 6-151
6.8.1. Nyers adatok. ............................................................................................................................. 6-152
6.8.2. Hivatkozás. ActionLink és RouteLink .......................................................................................... 6-153
6.8.3. Űrlap. BeginForm ....................................................................................................................... 6-154
6.8.4. Szövegbevitel. TextBox, TextArea .............................................................................................. 6-156
6.8.5. Label és formázott megjelenítés ................................................................................................ 6-159
6.8.6. Legördülő és normál lista ........................................................................................................... 6-160
6.8.7. Jelölők és rádióvezérlők CheckBox ............................................................................................. 6-163
6.8.8. Editor és Display template-ek .................................................................................................... 6-164
6.8.9. Partial és Render Partial ............................................................................................................. 6-176
6.8.10. Validációs üzenetek megjelenítése ............................................................................................ 6-178
6.9. URLHELPER ........................................................................................................................................ 6-179

7. ASZINKRON ÜZEM, AJAX ............................................................................................................ 7-183

7.1. KERETRENDSZEREK TÁRHÁZA .................................................................................................................. 7-183


7.2. A JSON ............................................................................................................................................. 7-184
7.3. JQUERY DIÓHÉJBAN .............................................................................................................................. 7-184
7.4. AJAX HELPEREK ................................................................................................................................... 7-187
7.5. AJAX HELPEREK DEMÓ .......................................................................................................................... 7-192
7.6. JSON ADATCSERE ................................................................................................................................ 7-201
7.7. AZ MVVM KERETRENDSZEREKRŐL.......................................................................................................... 7-212

8. A MODEL BINDER ....................................................................................................................... 8-215

8.1. EGYSZERŰ TÍPUSOK ÉS A BEÉPÍTETT LEHETŐSÉGEK ....................................................................................... 8-216


8.2. FELSOROLÁSOK, LISTÁK ÉS SZÓTÁRAK ....................................................................................................... 8-223
8.3. BONYOLULT MODELLEK PROBLÉMÁI......................................................................................................... 8-230
8.4. MÉLYEN BELÜL .................................................................................................................................... 8-232

9. A BIZTONSÁG ÉS AZ ÉRTELMES ADATOK .................................................................................... 9-240

9.1. A RENDSZER BIZTONSÁGA ...................................................................................................................... 9-240


9.2. A FRONTVONAL ................................................................................................................................... 9-241
9.3. FELHASZNÁLÓ HITELESÍTÉS ..................................................................................................................... 9-245
9.3.1. Form alapú hitelesítés ................................................................................................................ 9-246
9.3.2. Windows alapú hitelesítés ......................................................................................................... 9-255
9.3.3. OAuth, OpenID ........................................................................................................................... 9-258
9.4. KÓDOLT AZONOSÍTÓK ........................................................................................................................... 9-261
9.5. VALIDÁLÁS ......................................................................................................................................... 9-273
9.5.1. A szerver oldalon ........................................................................................................................ 9-274
9.5.2. A kliens oldalon .......................................................................................................................... 9-285
1-5

10. REAKCIÓKÉPESSÉG, GYORSÍTÁS, MINIMALIZÁLÁS. ................................................................... 10-297

10.1. AZ OUTPUTCACHE ............................................................................................................................. 10-298


10.2. AZ ADAT CACHE ................................................................................................................................ 10-306
10.3. A BUNDLING..................................................................................................................................... 10-312

11. REAL WORLD ESETEK ................................................................................................................ 11-319

11.1. TÖBBNYELVŰ ALKALMAZÁS .................................................................................................................. 11-319


11.2. AZ ALKALMAZÁS MODULARIZÁLÁSA. AZ AREA. ........................................................................................ 11-327
11.3. MOBIL NÉZETEK, VIEW VARIÁNSOK ....................................................................................................... 11-333
11.4. SAJÁT HTML HELPEREK, MODELL METAADATOK ....................................................................................... 11-338
11.5. FÁJL LE- ÉS FELTÖLTÉS ......................................................................................................................... 11-343
11.6. DOLGOZZUNK EGYEDI VIEW SABLONOKKAL! ............................................................................................ 11-356

12. MVC 5 ÚJDONSÁGAI ÉS VÁLTOZÁSAI ....................................................................................... 12-358

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

A 40-es éveinek az elején járó szoftverfejlesztő vagyok. A


programozással 13 éves koromban ismerkedtem meg, mikor kaptam
egy könyvet születésnapomra. Valamilyen Basic jellegű nyelvről volt
benne szó, és biztos vagyok benne, hogy az ajándékozó nem tudta mit
vett. A programnyelv nevére sem emlékszem már, csak arra, hogy
elképesztő megszállottsággal vetettem bele magamat. Addig csak egy
néhány lépésre képes, "programozható", szovjet számológépet
próbálgathattam. Még TOS alapú számítógépet is csak a TV-ben láttam, így az egészet csak virtuálisan
a fejemben tudtam elképzelni hardverestől-szoftverestől. Nagyon izgalmas volt, mert egy teljesen
másik világban éreztem magam. A változók, regiszterek, goto-k, szubrutinok csak forogtak körülöttem.
14 évesen HT-1080Z1 csodagépen írtam az első kódokat az OMIKK-ban. Ekkor még sorba kellett állni a
gépidőért…

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ó.

Aláírás helyett, a digitális nyomaim:

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

1.3. Hasznos dolgok

A könyv legújabb verziója letölthető innen: http://bit.ly/mvc4plus

Igyekszem majd a visszajelzések, javaslatok alapján kiegészíteni az aktuális


verziót és időnként frissíteni a feltöltött tartalmat. Emiatt célszerű időnként
letölteni az aktuális változatot.

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 linkje: http://bit.ly/mvc4-app

A letöltést a fájlt kiválasztva a helyi menün


keresztül, vagy a fejlécmenü Letöltés linkéve
kattintva lehet elindítani. Eltelhet 10-20
másodperc is mire a letöltés elindul.
Lehetséges, hogy a projektek megnyitása után
a Visual Studio még egy IIS Express-t is le fog tölteni, mert erre a
fejlesztői webszerverre vannak beállítva.

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.

A könyvvel kapcsolatos javaslatok, észrevételek email címe: mvc4@cornelius.hu. Idevárok minden


véleményt. Jót is rosszat is, mert a semminél még egy negatív vélemény is jobb…

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

1.4. A rövidítésekről, nevekről és jelekről

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.

A képi illusztrációkat Visual Studio 2012-vel betöltött


példaprogram alapján készítettem, vágtam ki. Az ikonok a
régebbi VS verziókban mások voltak. A képen látható zöld
pluszok és piros pipák az általam használt verziókövető
rendszer 3 állapotjelölői, nincs semmi jelentőségük a
példákkal kapcsolatban.

Ahogy már említettem az a hamarosan elérhető új változatot jelenti, de sajnos a hivatkozott


képességek egy része a jelenleg elérhető VS2013 preview változatban sem használhatóak még. Az
abban levő MVC assembly 5.0-ás ugyan, de messze nem a végleges változat. Ezért ezeket a
képességeket csak úgy lehet kipróbálni, ha lefordítjuk az aktuális MVC 5 változatot.

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.

2.1. A tendenciák áttekintése

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

2.2. A webes alkalmazásokról általában

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:

http://peldaoldal.hu/index.html -> c:\inetpub\peldasite\index.html.

Ez a szerver és a website konfigurációjának a függvénye. Ez az un. ’resource mapping’ mostanában


ritkán ilyen egyszerű. Jellemzően az URL-t szakaszonként értelmezik és így lehetőség van a szakaszok
szerinti mappa/erőforrás leképzésre.

Erre egy példa:

http://peldaoldal.hu/termekek/index.html -> c:\inetpub\kozostermekek\index.html


http://peldaoldal.hu/csoportok/index.html -> c:\inetpub\focsoportok\lista.html

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.

Unfriendly URL: http://peldaoldal.hu/termekek.aspx?csoport=butorok&alcsoport=szekek&labak=3


Friendly URL: http://peldaoldal.hu/termekek/butorok/szekek/labak/3

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.

Ahhoz, hogy a speciális böngészőkéréseknek speciálisan tudjon válaszolni a szerver, bővítményekkel


(plugin, handler, module) lehet kiegészíteni. Ezek a bővítmények képesek a webszerver normál
kiszolgálási mechanizmusát a speciális kéréstípusnak megfelelően lekezelni. Ezek a kéréstípusok
leginkább a kért erőforrás fájlnév kiterjesztése alapján kategorizálhatóak. Így például lehet olyan
(HTTP) handlert készíteni, ami dinamikusan generálandó képfájlt tud készíteni. Például olyan képet,
ami tartalmazza a mai dátumot, ahelyett hogy a webszerver a fájlrendszerben tárolt fájlt szolgálna ki.
Szintén megoldhatjuk egy ilyen handlerrel, hogy a böngészőnek küldendő HTML oldalt memóriában
állítsuk össze, oldalsablonok alapján.

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.

2.3. Böngésző – szerver interakció

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.

GET, PUT, POST, DELETE, HEAD, stb.

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.

GET http://domainev.hu/termek/10?q=vendeg HTTP/1.1

- 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.

Ha a fenti HTTP igéket és az erőforrásokat reprezentáló URL-eket szervezetten használjuk, tehát ha a


HTTP method lesz az ige és az erőforrásunk a tárgy, akkor a rendszerünk nem lesz REST úgy használni
a web-et, ahogy azt eltervezték. Nagyjából ez a REST jelentése is. Példaként adott egy URL:
http://domainnev/termek/1, ami az 1-es azonosítószámú terméket teszi elérhetővé. A HTTP
methoddal pedig közölhetjük, hogy mit tegyen a szerver ezzel az 1-es számú termékkel. Letöltse
(GET), frissítse az adatait (POST), törölje (DELETE). Postback-nek szokták nevezni azt a szituációt,
2.4 Bevezetés - Az előzmény. Az ASP.NET Web Forms 1-13

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).

2.4. Az előzmény. Az ASP.NET Web Forms

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!

 A kezdeti alapötlet arra épült, hogy a Windows Forms/MFC/Delphi környezetben felnőtt


szakemberek, hogyan tudnák a dizájnerrel támogatott RAD (gyors alkalmazásfejlesztés)
metodikában, rutinosan használt fogásaikat megtartva, gyorsan átállni a webes fejlesztésre.
Nem mondhatjuk, hogy ez nem volt sikeres. Az a lehetőség, hogy a szerkesztői felületre
dobhatjuk a vezérlőt és néhány kattintás után egy-két propertyt beállítva kész az oldal, nagyon
rapiddá teszi a fejlesztést. Ott van még az eseménykezelés, az egyszerű adatkötés, a felületre
húzható adatszolgáltatók. Ilyenekről egy PHP-s fejlesztő nem is álmodik. Remélem, nem ijeszt
el senkit, de már most elárulom, hogy az ASP.NET MVC sem támogatja ezeket. Ugyanis ez egy
másik szempontot támogat, a jól kézben tartott kliens oldali kódot, és a funkcionális rétegek
elszeparálását. A fejlesztő pedig tanuljon meg HTML-t és JS kódot írni…
 Amikor megnézünk egy kész, főleg régebben készült ASP.NET Web Forms alkalmazást, két
feltűnő dolgot lehet észrevenni a generált HTML kódban. Az egyik, hogy a kód kicsit kusza és
tele van beékelt javascript kódblokkal. Emiatt nem túl egyszerű hibát keresni benne. A másik,
hogy szinte minden felületi elrendezést <table> HTML elemmel oldanak meg, ami a SEO
korában nagyon nem ajánlott megközelítés. Sajnos valahogy ez szokássá vált. Többször volt az
az érzésem, amikor egy ASP.NET fejlesztő a HTML-ről beszélt, mintha csak nyűg lenne az egész
HTML és JS környezet. Miközben PHP körökben szabályos, szabványos oldalak születnek.
Némelyiknek öröm nézni a kódját.
2.4 Bevezetés - Az előzmény. Az ASP.NET Web Forms 1-14

 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.

2.5. ASP.NET MVC platform előnyei

Nézzük meg miben más vagy jobb az MVC keretrendszer.

- 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ű.

2.6. Az ASP.NET és MVC framework

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.

2.1. Az MVC komponensei és beszerzésük.

A kalandtúra első lépése, hogy összeszedjük a felszerelést.

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

működik és jelentősen leegyszerűsíti a környezet beállítását és a további szükséges kiegészítők


letöltését is. Ezt ajánlott használni. A másik a standalone telepítő, amivel szintén lehet telepíteni, de a
függőségekre ekkor nekünk kell figyelni, és egyesével kell telepíteni azokat. Visual Studio 2010-es
esetén szükséges, hogy a Visual Studio 2010 Service Pack 1 előzőleg már telepítve legyen. A
működéshez a .NET 4 és a PowerShell 2.0 (minimum) szintén alapfeltétel.

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

 Hol kell beállítani a preferált nyelvet


 Hol kell kitörölni a böngészési előzményeket és a cookie-kat.
 Hol kell letiltani a javascript futtató motort
 Hogy lehet elővarázsolni a belső diagnosztikai lehetőségeket.

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!

Általában ne a és ehhez hasonló ikont használjuk oldalfrissítésre. Ezeket nem a fejlesztőknek


szánták. Van helyette F5 billentyű és Ctrl+F5 kombináció is. Böngészőnként kicsit eltérnek, de
hatásukra újratöltődik az oldal, de a Ctrl+F5 FireFox esetében kényszerítetten újratölti a helyben tárolt
adatokat is. Ezt érdemes kipróbálni a Net fülön (tab-on). Egy gyakori kezdő fejlesztői hiba, hogy az átírt
CSS vagy JS kód nem azt csinálja amit módosítottunk, hanem makacsul az előző változatot hozza. Ennek
oka, a böngészőben levő gyorsítótárazás. Ennek eredménye bekeretezve látható a következő ábrán
"304 – Nincs módosítva". Ami azt jelenti, hogy a tartalom a helyi gyorsító tárból jött és nem a
szerverről, ahova feltöltöttük a legújabb verziónkat. Erre érdemes figyelni a tanuláskor, fejlesztéskor
és a webszerver beállításakor!

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.

Innentől viszont kezdjünk el foglalkozni a közelmúlt egyik legnagyszerűbb webfejlesztési


technológiájával, amit a Microsoft kiadott.

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.

3.1. Az MVC architektúra

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).

A M-V-C hármas tagolás nagyban segíti a szakterületekre specializálódott fejlesztői munkacsoportok


kialakítását. Így lehetőségünk van arra, hogy a View megvalósításánál olyan szakértőket alkalmazzunk,
akik a felhasználó élmény, a szép és interaktív felületek kivitelezésében jártasak, ők a front-end
fejlesztők. Míg egy másik csapat, a back-end fejlesztők, a front-end-et kiszolgáló szolgáltatások és
adatsémák megalkotásában zsonglőrködnek. Ügyes tervezéssel pedig biztosítható, hogy a csapatok
munkája közel párhuzamosan haladjon. Hamarosan nyilvánvalóvá fog válni, hogy egy View
megtervezése és megalkotása számos további technológiát igényel, amik együttesen elég nagy

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

Controller+Action View feldolgozása

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.

public class Profile


{
public string Name { get; set; }

public string Email { get; set; }

public DateTime BirthDate { get; set; }


}
1. példakód

Nagyon fontos a definiált tulajdonságok (propertyk) jó elnevezése. Ugyanis durva közelítéssel, de a


folyamat végén a generált HTML elemek elnevezései, azonosítói ebből fognak képződni. Az osztály
elnevezése nem kötött, de sokszor szokták kiegészíteni a ’Model’ utótaggal, így a példában lehetett
volna ProfileModel is. Ez az elnevezési konvenció végigkíséri az MVC hármast. Érdemes követni, mert
így jól áttekinthető kódot kapunk. Például a felhasználói profilkezelés esetében rögtön tudjuk, hogy
mely MVC elemek tartoznak össze, ha így nevezzük el:

ProfileModel, ProfileController és Profile mappa a View-k számára.

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

A View kódjában kiaknázhatunk olyan HTML-t generáló metódusokat, amelyek az modellosztályunkon


vagy annak tulajdonságain definiált metainformációkat is képesek értelmezni és nagyszerűen
felhasználni. Ezeket a metainformációkat természetesen attribútumokban (attribute) tudjuk leírni a
.Net lehetőségei miatt. A tulajdonságok metainformációi leginkább a validációra szoktak vonatkozni,
de lehetséges befolyásolni a megjelenítést, a szerkeszthetőséget és sok más jellemzőt. Kicsit
kidekorálva az előző osztálydefiníciót:

public class Profile


{
[Required]
[Display(Name = "Felhasználó név")]
[StringLength(100, ErrorMessage = "A {0} legalább {2} karakter hosszúnak kell lennie.",
MinimumLength = 6)]
public string Name { get; set; }

[Display(Name = "Email cím")]


public string Email { get; set; }

[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:

Látható az attribútumok hatása,


amiknek az elnevezése elég
beszédes.

Meg tudjuk határozni a beviteli


mező felett megjelenő címke
tartalmát (DisplayAttribute()).
Előírhatjuk, hogy a mező kitöltése
2. ábra
1 kötelező (RequiredAttribute), vagy
a beírható karakterek számát
korlátozhatjuk (StringLengthAttribute). Sőt azt is közölhetjük, hogy a születési időt ne csak egy
textboxban jelenítse meg, hanem egy általunk megírt megjelenítési formában, aminek itt most nem
látszik a hatása.

Az ilyen attribútumokat a System.ComponentModel.DataAnnotations névtér tartalmazza, nem


kötődnek az MVC-hez, más technológiákban is kihasználhatjuk, esetleg onnan is ismerős lehet. A
születési idő Required attribútuma nincs jól felparaméterezve így kissé furcsa az alapértelmezett
hibaüzenet, de hasonlóan a StringLength-nél alkalmazott lehetőséghez itt is használható lenne az
ErrorMessage tulajdonság magyarra fordított tartalmának megadása.

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:

public ActionResult Edit(Profile inputmodel)

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.

Tisztelt [CÍMZETTNEVE]!  Tisztelt Claude Debussy!

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:

<%@ Page Language="C#" Inherits="System.Web.Mvc.ViewPage<MvcApplication1.Models.Profile>"


%>
<!DOCTYPE html>
<html>
<head runat="server">
<title>Profile ASP</title>
</head>
<body>
<% using (Html.BeginForm()) { %>
<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>
3. példakód
3.3 Első megközelítés - A View 1-24

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:

@model MvcApplication1.Models.Profile és összevetni a Web Forms megfelelőjével, amit az előbb


néztünk. A későbbiekben a példakódok ebben a Razor stílusban lesznek bemutatva. Részletesen a 6.6.1
alfejezet mutatja be a használatát és a rejtelmeit.
3.4 Első megközelítés - A Kontroller 1-25

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:

public class HomeController : Controller


{
public ActionResult Index()
{
ViewBag.Message = "Modify this template to jump-start your ASP.NET MVC application.";
return View();
}

public ActionResult About()


{
ViewBag.Message = "Your app description page.";
return View();
}

public ActionResult Contact()


{
ViewBag.Message = "Your contact page.";
var contactModel = new MvcApplication1.Models.ContactModell();
contactModel.FirstName = "Kapcsolat";
contactModel.LastName = "Tartó Gizi";
contactModel.PhoneNumber = "0690000000";
return View(contactModel);
}

public ActionResult Profile()


{
return View(new Profile());
}
}
5. példakód

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.

Az alapképlet ennyi. Ugye, hogy nem bonyolult?


3.5 Első megközelítés - Próbáljuk ki! 1-26

3.5. Próbáljuk ki!

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

OK gomb megnyomása után válasszuk ki a konkrét projekt template-et.

4. ábra

A projekt template-ek által létrehozható alkalmazásváltozatok:

 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.

Nálam így nézett ki az


MVC4-es projekt
futtatásának eredménye. A
jobb felső sarokban a
felhasználó alapműveletei,
alatta három menüpont
(Home, About, Contact)
látható. Ezek a
menüpontok kész
oldalakra visznek. A Home
vissza visz a nyitólapra.
3.6 Első megközelítés - Az alkalmazás felépítése 1-28

3.6. Az alkalmazás felépítése

Nézzük meg milyen projektet hozott létre az előbbi varázslás!

A projekt egy mappákkal jól strukturált projektből áll. Fentről lefelé


haladva nézzük meg ezeket és a céljukat.

App_Data –t adatbázis fájlok tárolására lehet használni. Most még nincs


tartalma.

App_Start beállító kódok gyűjteménye, amik az alkalmazás első


indulásakor kapnak szerepet.

Content-ben jellemzően statikus, nem fordítandó fájlok vannak. Képi


elemek, CSS fájlok, amiket minden további nélkül le lehet tölteni a
böngészőbe. A Site.css a stílus sablon.

Controllers tartalmazza a kontrollerek osztályait.


7. ábra
6.
Filters-be kerülhetnek az Actionök viselkedését, elérhetőségét szabályzó
attribútum definíciók.

Images hasonlóan a Content mappához, statikus képfájlokat szokott 5. ábra


tartalmazni.

Models, mint neve is sugallja, a modell deklarációk gyűjtőhelye.

Scripts a javascriptek fájljainak mappája.

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

3.7. Új Model, View, Controller hozzáadása

A projektvarázslóval legyártott projektet megnéztük és láttuk, hogy egy azonnal bővíthető


minialkalmazást kaptunk, készre sütve. Az MVC a Visual Studio-val együttműködve további könnyed
lehetőségekkel támogatja, hogy az alkalmazásunkat új oldalakkal bővítsük. Célszerű azzal folytatni,
hogy meghatározzuk, hogy milyen adatot szeretnénk megjeleníteni. Ehhez készítsünk egy tetszőleges
modellosztályt néhány propertyvel, és tegyük a Models mappába. Annyit érdemes már ilyenkor
eldönteni, hogy milyen elnevezést használunk az oldalunk számára, mert ezt a nevet célszerű
végigvezetni a kontroller és a View elnevezésén is.

Ez az elnevezés most a 'First' lesz.


Modellt a hagyományos módon
adhatunk hozzá. A modell neve
most 'FirstModel' lesz.

public class FirstModel


{
public int Id { get; set; }
public string FullName { get; set; }
public string Address { get; set; }
}

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.

Create(FormCollection collection) – Ez lehet az az action, ami menti az új elemet. Valójában csak a


paraméterek megléte és egy HTTPPost attribútum mutatja, hogy ez fogadja az újonnan létrehozott,
kitöltött formot.

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.

Most, hogy megvan a modellünk és a kontrollerünk is, fordítsuk le az alkalmazásunkat, hogy a


modelldefinícióval együtt létrejöjjön a projekt dll fájlja. Erre szüksége lesz a következő lépésnél a VS-
nak.

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.

A dialógus ablakban állítsuk be a következő képnek megfelelően a paramétereket.


3.7 Első megközelítés - Új Model, View, Controller hozzáadása 1-31

Mivel az Index action View() metódushíváson


kértük a helyi menüt, emiatt a View neve is
'Index' lesz, miután megjelenik az ablak. Ez jó is
így, és a továbbiakban is jó lesz ha ezt nem
állítjuk át. Kell egy pipa a 'Create a strongly-
typed view' checkboxra. A 'Model class'
legördülő listában válasszuk ki a modellünket.
Ha nincs itt meg a modell, akkor valószínűleg
nem fordítottuk le a projektet. Nem probléma,
ezt a lépést még gyakorlott fejlesztők is el
szokták felejteni. Ebben az esetben a
dialógusablakot be kell zárni és a fordítás után
újrakezdeni az Add View… menüponttal. A
legalsó nyíllal jelölt listában válasszuk a 'List'-et.
Az Add gomb hatására elkészül a View.

9. ábra

Nézzünk rá a kész View első sorára: @model IEnumerable<FirstMVCApp.Models.FirstModel>

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.

public ActionResult Index()


{
var listmodel = new List<Models.FirstModel>();
listmodel.Add(new Models.FirstModel()
{
Id = 1,
FullName = "Karcsi",
Address = "Hosszú utca 1."
});
listmodel.Add(new Models.FirstModel()
{
Id = 2,
FullName = "Pista",
Address = "Hosszú utca 3."
});
//Adjuk át a modellt a View-nak
return View(listmodel);
}

A View létrehozásakor a View .cshtml kiterjesztésű fájlját a kontroller


nevével megegyező almappába tette a Views mappán belül. A „First”
mappát is létrehozta nekünk.

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.

A Details actionhöz a Details template illik,


ahogy a képen is látható:

A View name és minden más is jól van kitöltve.


Ami változik az csak a template actionről-
actionre haladva:

Details View – Details template


Create View – Create template
Edit View – Edit template
Delete View – Delete template

Elrontani sem lehet. A View sorozatgyártás eredményeként létre kell jönnie


egy ilyen fájllistának a Views/First alatt:

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

public ActionResult Edit(int id)


{
var model = new Models.FirstModel()
{
Id = 1,
FullName = "Karcsi",
Address = "Hosszú utca 1."
};
return View(model);
}

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.

A képen az Edit action/View eredménye látható miután rákattintottam


az Index oldalon levő első sor Edit linkjére. A nyilak a mezőfeliratokat
mutatják. Ami pontosan megegyezik a modell property nevével. Alatta
vannak a modell propertyk alapján készült szöveges beviteli mezők
(textbox). Azon, hogy a mezőfeliratok ne ilyen nyersek legyenek, a
modell propertykre helyezett Display attribútumokkal tudunk segíteni.

public class FirstModel


{
public int Id { get; set; }

[Display(Name = "Teljes név")]


public string FullName { get; set; }

[Display(Name = "Szállítási cím")]


public string Address { get; set; }
}

Így már jobb a megjelenés. Sőt az


Index oldal listájának az oszlop
felirata is jobb lett:

A Save gomb és az alatta levő "Back to List" link is egyszerűen átírható.


(Mondjuk a submit gomb feliratának az átírásában, nincs semmi MVC
specifikus)

<div>
@Html.ActionLink("Back to List", "Index")
</div>

Átírva:

<div>
@Html.ActionLink("Vissza a listához", "Index")
</div>

Hasonló módon átírhatjuk a lista utolsó oszlopainak (Edit,Details,Delete) a feliratát is:

@foreach (var item in Model)


{
<tr>
<td>
@Html.DisplayFor(modelItem => item.FullName)
</td>
3.7 Első megközelítés - Új Model, View, Controller hozzáadása 1-34

<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&#233;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:

public ActionResult Edit(int 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>

@Html.HiddenFor(model => model.Id)

<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

@Html.EditorFor(model => model.FullName)


@Html.ValidationMessageFor(model => model.FullName)
</div>

<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.

Ebben a műveletsorban létrehozott actionök és View-k segítségével a FirstModel osztályunkkal


végezhető általános műveleti igényekre elkészítettük a felületeket. Lehet listázni, megnézni,
szerkeszteni és törölni a részletes adatait. Legalábbis majdnem, mert a modellel nem történik semmi,
mert nem kerül elmentésre. A folytatáshoz elég ennyit is megérteni. Ha az olvasónak ez volt az élete
első MVC oldala, mielőtt továbbhaladna, azt javaslom, hogy készítsen még további saját oldalakat,
kontrollereket, modelleket az előzőekben bemutatott lépések alapján. Próbáljon új linkeket létrehozni,
komolyabb modelleket írni és felhasználni. Ebben az esetben most nem kell sietni. Célszerű lerakni ezt
az írást és játszadozni a lehetőségekkel.
3.8 Első megközelítés - Próbáljuk ki menthető adatokkal! 1-36

3.8. Próbáljuk ki menthető adatokkal!

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.

Hozzunk létre egy új MVC internet projektet a 3. ábra szerint.

Hozzunk létre egy új modellt

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:

[Display(Name = "Teljes név")]


[StringLength(100)]
[Required]
public string FullName { get; set; }

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:

public class CardRegister


{
public CardRegister()
{
PhoneNumbers = new List<PhoneNumber>();
}

public int Id { get; set; }

[Display(Name = "Teljes név")]


[StringLength(100)]
[Required]
public string FullName { get; set; }

[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; }
}

public class PhoneNumber


{
public int Id { 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 két tábla 1:n kapcsolatban van egymással, a CardRegister.Id és a PhoneNumber.CardRegisterId közti


relációval. A 'PhoneNumbers' navigation propertyben az adott CardRegister-hez tartozó
PhoneNumberek lesznek felsorolva. Illetve a 'CardRegister' backreference property referenciát tárol
arról, hogy az adott telefonszám melyik névjegykártyához tartozik.

A modellen kívül szükség lesz az adatbázis tábla összerendelést és az adatkontextust definiáló


osztályra:

public class CardRegisterDb:DbContext


{
public CardRegisterDb() :base("CardRegisterDatabase") { }

public DbSet<CardRegister> CardRegisters { get; set; }


public DbSet<PhoneNumber> PhoneNumbers { get; set; }
}

A property nevek ebben az osztályban egyben az adatbázis táblaneveit is jelentik. Az ősosztály


konstruktorának átadott név lesz az adatbázis fájl neve. Ha kész vannak a modellek és az
adatkontextus, mindig az a lépés következik, hogy le kell fordítani a modellt tartalmazó projektet.
3.8 Első megközelítés - Próbáljuk ki menthető adatokkal! 1-38

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:

A bal oldali ábra szerint állítsuk be a


mezőket és adjunk meg egy
konzekvens nevet a kontroller
számára. Amire figyelni kéne, az a
'Template' lista pontos beállítása,
mert több hasonló nevű eleme van.

Az 'Add' gombbal el fog készülni a


kontroller, és a kontroller action
metódusai számára az összes View fájl
is.

Tulajdonképpen ezzel kész is vagyunk. A CardReader/Index oldalt meg is


nyithatjuk és működni fognak a listázó, szerkesztő, törlő funkciók. Még talán
érdemes a _Layout.cshtml-ben a felső menüpontokhoz hozzáadni az új Index oldalra mutató linket:

<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 FullName ('Teljes név') property a


Required attribútum miatt kötelezően
kitöltendő.

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.

Ha értelmes adatokat adunk meg, a 'Create' gombra kattintva


elmentődnek az adatbázis tábla sor mezőiben, és létrejön az új
névjegykártya.

A 'Details' linkkel megnyílik az oldal, ami


nagyon össze van esve. Sebaj. Nyissuk meg a
Content/site.css fájlt, ami az alkalmazás
stílusdefiníciója, és írjuk be a végére ezeket CSS
osztálydefiníciókat.

Az eredményt a jobb alsó kép mutatja.

.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;
}

Tudunk létrehozni, menteni, törölni CardRegister elemeket. Most


vessünk egy pillantást a projektre és az App_Data mappa alatt
megtalálhatjuk az időközben létrejött háttéradatbázist. A 'Show All
files' gombbal tudjuk megjeleníteni, mert nincs a projekthez
csatolva. (felső képsáv) Ha duplán kattintunk az .mdf fájlra,
megnyílik a Server Explorer és benne megtaláljuk a létrejött
táblákat.
3.8 Első megközelítés - Próbáljuk ki menthető adatokkal! 1-40

Ott van minden: az adatbázis név tényleg a CardRegisterDb


konstruktorában levő név szerinti, a táblanevek pedig a CardRegisterDb
property nevek szerint jöttek létre.

A nagy varázslat nem csinálta meg számunkra azt, hogy a


névjegykártyához telefonszámokat is tudjunk rendelni. Innen már
nekünk is kell csinálni valamit. Azonban hogy ne legyen annyira
fájdalmas, felhasználjuk megint a varázslót, hogy valami alapot
készítsen számunkra.

Tehát hozzunk létre egy PhoneNumberController-t a PhoneNumber modell alapján.

Létrejönnek megint a
View fájlok is.

Meg is nézhetjük. Lehet létrehozni, szerkeszteni a PhoneNumber elemeket.


Sőt még hozzá is rendelhetjük a telefonszámot valamelyik névjegykártyához
a legördülő listával. Mindössze az rontja el az összképet, hogy a
'Névjegykártya' feliratnak kéne megjelennie a piros nyíllal jelzett helyen.

Nyissuk meg a PhoneNumber/Create.cshtml-t, keressük meg ezt a sort és


töröljük ki, amit áthúztam:

<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.

A névjegykártya részletes listájában legyenek felsorolva a telefonszámok is.

Ehhez a Views/PhoneNumber/Index.cshtml-t másoljuk le és nevezzük át PhoneNumberPartial.cshtml-


re, és tegyük át a Views/CardReader mappába. A belsejét kicsit át kell alakítani. A lenti kódban
áthúztam ami törlendő és vastagon van szedve, amit hozzá kéne írni. (ActionLink sorok)

@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

@Html.DisplayNameFor(model => model.Number)


</th>
<th>
@Html.DisplayNameFor(model => model.CardRegister.FullName)
</th>
<th></th>
</tr>

@foreach (var item in Model) {


<tr>
<td>
@Html.DisplayFor(modelItem => item.Number)
</td>
<td>
@Html.DisplayFor(modelItem => item.CardRegister.FullName)
</td>
<td>
@Html.ActionLink("Edit", "Edit", "PhoneNumber", new { id=item.Id}, null) |
@Html.ActionLink("Details", "Details", "PhoneNumber", new { id=item.Id }, null) |
@Html.ActionLink("Delete", "Delete", "PhoneNumber", new { id=item.Id }, null)
</td>
</tr>
}
</table>

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').

Következő lépésben, a Views/CardRegister/Details.cshtml fájlban a lezáró </fieldset> után, részleges


View-ként hivatkozzunk az előbbi fájlra (partial View-ra):

</fieldset>

<div class="phones">
@Html.ActionLink("Új telefonszám", "Create", "PhoneNumber", new { cardid = Model.Id }, null) <br/>
@Html.Partial("PhoneNumberPartial", Model.PhoneNumbers)
</div>

A partial View neve mellett átadásra kerül az aktuális névjegyhez


tartozó telefonszámok listája (PhoneNumbers), hisz ezt kell
megjeleníteni. Az eddigi lépések eredménye látható a jobb oldali
képen, miután már létrehoztam néhány új telefonszámot.

Az első ActionLink sor ('Új telefonszám') segítségével tudjuk majd


indítani a PhoneNumber kontroller Create actionjét, úgy hogy a
'cardid' paraméterébe átküldjük az aktuális névjegy azonosítóját
(cardid = Model.Id). Most még nincs neki. Emiatt értelmesen kell
módosítani a Create metódust, hogy tudjon létrehozni a
hivatkozott névjegyhez új telefonszámot, és a régi képessége is megmaradjon.

public ActionResult Create(int? cardid)


{
ViewBag.CardRegisterId = new SelectList(db.CardRegisters, "Id", "FullName", cardid);
return View(new PhoneNumber() {CardRegisterId = cardid ?? 0});
}

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.

Amikor a kitöltött telefonszámot tartalmazó form az alábbi Create az actionhöz


küldi a mezői tartalmát, a form mezői alapján feltöltött PhoneNumber objektumot
kapjuk meg metódusparaméterként. Ez az objektum olyan állapotban van, hogy
minden további nélkül menthetjük is az adatbázisba. (Add(…) és SaveChanges()
metódushívások).

[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");
}

ViewBag.CardRegisterId = new SelectList(db.CardRegisters, "Id", "FullName",


phonenumber.CardRegisterId);
return View(phonenumber);
}

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.

@Html.ActionLink("Edit", "Edit", new { id=Model.Id }) |


@Html.ActionLink("Vissza a névjegykártyához", "Details","CardRegister",
new {id = Model.CardRegisterId}, null)

Azaz vissza a CardRegister kontroller Details action metódusához úgy, hogy


a metódus 'id' paramétere legyen feltöltve a Model.CardRegisterId
értékével.

Ez a néhány oldalas bemutatót figyelem felkeltésnek szántam. Remélem


sikerült megmutatni, hogy az MVC nagyon jól együtt tud működni külső
adatforrással, kiváltképp az Entity Framework-kel.
3.9 Első megközelítés - A projekt beállításai 1-43

3.9. A projekt beállításai

Érdemes megnézni, hogy milyen alapértelmezett beállításokkal


jönnek létre az új MVC projektek. Ha a Solution Explorerben a projekt
nevét kiválasztjuk és nyomunk egy Ctrl+Enter kombót vagy a jobb
egér gombbal a helyi menüből a ’Properties’ pontot választjuk,
megjelennek a projekt tulajdonságai. Nézzük meg a lényeges
beállítási lehetőségeket. Ezekről már most jó, ha tudomást szerzünk.

Az első az Application fül tartalma:

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.

A Build fül alatt csak az ’Output path’-t emelném ki.

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.

A fejlesztés során a leglényegesebb beállítások a Web fül oldalán vannak.

A ’Specific Page’ beállítása alapértelmezetten üres. Fontos


tudni, hogy itt meg lehet adni kezdő oldalt a projektünkben
levő fájlok és elérési utak közül, ami akkor indul el, amikor az
alkalmazásunkat a Visual Studio-ból indítjuk. Az MVC nem fájl
alapon szolgálja ki a kéréseket, ezért ide ne írjunk olyan
elérési utat, aminek a végén egy fájl található, mert ez a
beállítás az ASP.NET Web Forms fejlesztés esetén hasznos. Az
MVC-ben mindig kontrollert és actiont kell megcéloznia az
URL-nek. Ilyet lehet beírni ide: Home/About (Nem kell a
Home elé / jel). A másik módszer arra, ha azt szeretnénk, hogy a fejlesztés során ne mindig a kezdő
oldal jelenjen meg és innen kelljen továbbnavigálni a fejlesztés/tesztelés alatt levő oldalra, akkor
válasszuk a ’Start URL’-t és írjuk be a teljes URL-t pl.: http://localhost:18005/Home/About
3.10 Első megközelítés - A MVC komponenseinek működési ciklusa 1-44

A Servers szekcióban beállítható a fejlesztés során


használandó webszerver és ennek a legfontosabb
paraméterei. A Visual Studio 2010-ig az alapértelmezett
beállítás a ’Use Visual Studio Development Server’ volt,
emellett használhattuk még az operációs rendszerre
telepített IIS webszervert 16 is. Ezen az ábrán a VS 2012-es
beállításai láthatóak. Az alapértelmezett most a VS-val
feltelepült ’Local IIS Web server’, ami valójában egy IIS
Express webszerver. Ez sokkal közelebb van minden
jellemzőjében a teljes értékű IIS webszerverhez. Emiatt az
alkalmazásunkat is jobban tudjuk tesztelni, mintha a VS
belső development server-ét használnánk. Várhatóan
kevesebb kellemetlen meglepetésben lesz részünk,
10. ábra
amikor majd a kész alkalmazásunkat az éles IIS szerverre
telepítjük. A ’Project Url’ a legfontosabb beállítás, ez határozza meg, hogy futásidőben a böngészőben
milyen URL-t kell megadni, hogy a futó webalkalmazásunkat címezze meg. A képen látható
’http://localhost:18005/’ beállítás volt az alapértéke a ’Start Url’ beállításnak.

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.

3.10. A MVC komponenseinek működési ciklusa

Nézzük végig nagyvonalakban hogy mi történik, ha az az előbb összeállított alkalmazásunk elindul.

 A projekt gyökerében levő global.asax-ban definiált MvcApplication-ünk Application_Start()


metódusa lefut és beállítja az MVC framework általunk meghatározott jellemzőit.
 A következő lépésben a csak domain nevet és portszámot tartalmazó URL (pl.:
http://localhost:18005/) alapján úgy dönt, hogy példányosítja a
Controllers/HomeController.cs-ben található osztályt és elindítja az Index() metódusát.
 Az Index() metódus még szinte semmit sem tartalmaz, csak meghívja a kontroller View()
metódusát, aminek a visszatérési értéke, egyben az Index() metódus visszatérési értéke is lesz.
 Az action metódus futása után betöltődik a Views/Home/Index.cshtml fájl és az MVC értelmezi
a tartalmát és elkészíti belőle a HTML markupot.
 A HTML tartalmat mint választ, visszaküldi a böngészőnek.
 Mikor az 'Első próba' linkre rákattintunk, egy teljesen új MVC ciklus indul el. Az eltérés csak
annyi, hogy az URL-ben megjelenik majd a /First (http://localhost:18005/First), ami a
FirstControllert jelenti. Az Index-et is odaírhatnánk: First/Index, de nem kell, mert az Index-et
odaérti az MVC, mert ez az alapértelmezett.
 Az Index actionben a modellt példányosítva adjuk át a View-nak, ami pedig felhasználja azt,
és az IEnumerable felsorolás elemein végiglépkedve listasorokat készít belőlük.

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

Következzen egy áttekintő térkép az actionök, modellek és View-k általános kapcsolatáról.

A bal oldali ábra egy másik szemszögből


mutatja be, hogy az MVC építőkövei hogyan
kapcsolódnak egymáshoz. Ezekről később lesz
részletesen szó, most csak szeretném mutatni,
hogy egyes főbb elemek, hol helyezkednek el
az alkalmazásunkban. Később úgy is minden
helyére kerül. Az MVC framework-öt a középső
sárga hétszög reprezentálja. Ehhez érkeznek a
kérések a böngészőtől. Jobbra és balra a mi
általunk írható kontrollerek a saját action
metódusaikkal láthatóak. Alul pedig egy
összetett View egymásba ágyazott template-
jeit mutatja.

A bal oldali Home kontroller rendelkezik


néhány action metódussal. Az Index action, a
példa kedvéért példányosított Home modellt
adja át az index.cshtml View fájlnak. Ezen belül további részleges View-k (partial View) vannak.

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…]

4.1. Modell és tartalom

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.

A modell használatával, tervezésével kapcsolatban összegyűjtöttem néhány ajánlást és megfontolandó


szempontot. Először is lássuk meg két pontban, hogy pontosan hol jelenik meg a modell.

 A View számára adatok tárolása:


Az általános szituáció, amikor a View által létre
szeretnénk hozni a dinamikus oldalt. A képen a
Home modell feladata, hogy tároló helyet
szolgáltasson a View-n megjelenő mezők adatai
számára. Ha szeretnénk egy ’felhasználónév’
mezőt megjeleníteni a HTML-ben, akkor a
modellben definiálunk egy propertyt hozzá. Ha
pedig egy táblázatot vagy egy comboboxot
szeretnénk megjeleníteni, akkor a modellben
definiálunk egy listát hozzá.
4.1 Modell - Modell és tartalom 1-47

 A böngészőtől érkező request paraméterek csomagja:

A másik eset, amikor a felhasználó


által kitöltött űrlap input mezőinek
az értékeit, az MVC a
modellobjektumba csomagolja,
megfeleltetve az input mezőket az
objektum propertyjeivel. Egy
további szituáció, amikor nem az
űrlap mezőinek az értékeit, hanem
JSON objektumot küld a böngésző
az MVC számára és az abban
definiált név-érték párokat párosítja a modell objektumunk tulajdonságaival.

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.

A modell tulajdonságai célszerűen publikus propertyk. A property nevek természetesen utaljanak a


tárolt adatra, de van néhány elv, amiket a későbbi problémák elkerülése miatt célszerű betartani:

 Í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.

public class WrongModel


{
public string Name { get; set; }
public string name { get; set; }
public string NAME { get; set; }

public string Action { get; set; }


public string Controller { get; set; }

public bool Checked { get; set; }


public bool Disabled { get; set; }
public string Form { get; set; }
public string Value { get; set; }
}
4.2 Modell - Modell és kód 1-48

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.

<form action="/Helper/HnameCollision" method="post">


<input id="Name" name="Name" type="text" value="Tanuló 1" /><br />
<input id="NAME" name="NAME" type="text" value="Tanuló 2" /><br />
<input id="name" name="name" type="text" value="Tanuló 3" /><br />

<input type="submit" value="Ment" />


</form>

Az előbbi formot a következő action metódusnak kéne fogadnia, de a WrongModel típusú


inputmodel paraméter mezőinek a kitöltése során Exception fog keletkezni.

public ActionResult HnameCollision(WrongModel inputmodel)


{
return View(model);
}

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".

public ActionResult HnameCollision(string Name, string name, string NAME)


{
return View(model);
}

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.

4.2. Modell és kód

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.

4.2.1. Viselkedéssel bővített modell

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.

public enum CustomerTypeEnum


{
Normal,
Supplier,
VIP
}

public class CarrierModell


{
public CustomerTypeEnum CustomerType { get; set; }

public DateTime OrderDate { get; set; }

public DateTime? TransportDate { get; set; }

public List<string> Arranges { get; set; }

//Számított értékek
public bool DeliveryDateAvailable
{
get { return TransportDate.HasValue; }
}

public string WarningMessage


{
get
{
return !TransportDate.HasValue && OrderDate.Date < DateTime.Today.AddDays(-2)
? "Késedelmes szállítás, azonnal intézkedj!" : String.Empty;
}
}

public string CustomerNameCSS


{
get
{
return CustomerType == CustomerTypeEnum.VIP ?
"vipcustomer" : "normalcustomer";
}
}
}
4.2 Modell - Modell és kód 1-50

A View kódját mellőzve a felhasználási értelmezésének pszeudo kódja ilyesmi lehet:

Ha van szállítási dátum (DeliveryDateAvailable), akkor jelenítsd meg a következő blokkot


{
Szállítási dátum feiratának és értékének a kiiratása
}

Ha nincs szállítási dátum és a megrendelés dátuma több mint két nap


{
Vastagon kiemelve egy szöveg, hogy ’Késedelmes szállítás, azonnal intézkedj!’
}

Ha vannak a szállítási út mentén további intézni valók vannak (Arranges)


{
Listázza az intézni való dolgokat egy táblázatban
}

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.

4.2.2. Üzleti logikával bővített modell

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

 Ha kéne kódot írni a View-ban, akkor azt tegyük inkább a modellbe,


 Ha sok lenne a kód a modellben, akkor legyen inkább a kontrollerben,
 Ha sok lenne a kód a kontrollerben, akkor legyen inkább egy szolgáltatásban vagy egy külön
segédosztályban.

Hogy kinek mi a sok kód, az leginkább tapasztalat kérdése. Ezek csak iránymutatások voltak.

4.2.3. A konstruktor probléma

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:

private otherObject=new OtherBigObject();

Ez azért is különösen veszélyes, mert innentől az OtherBigObject konstruktorára is vonatkozik a


"kontruktor probléma" tárgyköre, és ki tudja ki és mit fog az OtherBigObject-be tenni a későbbiekben.

17
http://en.wikipedia.org/wiki/Service-oriented_architecture
4.3 Modell - Modell és jellemzők 1-52

4.3. Modell és jellemzők

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

Ezeket az attribútumokat a részben a System.ComponentModel.DataAnnotations névtérben


találhatjuk részben az MVC részei. Sajnos a neveik elsőre nem sok tippet adnak, hogy csoportba milyen
sorolhatjuk be, ezért szétválogattam ezeket.

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:

[Display(Name = "Vásárló neve")]


public string FullName { get; set; }
4.3 Modell - Modell és jellemzők 1-53

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.

[Display(Name = "FullNameLabel", ResourceType = typeof(Resources.UILabels))]


public string FullName { get; set; }

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

Ez egy különleges attribútum. Használhatjuk közvetlenül, és akkor megadhatjuk a DataType enum-ban


felsorolt típusok közül valamelyiket (DateTime, Date, Time, Duration, PhoneNumber, Currency, Text,
Html, MultilineText, EmailAddress, Password, Url, ImageUrl). Vagy leszármazottakon keresztül is lehet
használni. Ráadásul egyszerre jelent megjelenítési és validációs szabályt 18 is. Erre egy jó példa a
DataType.EmailAddress:

[Display(Name = "Vásárló email")]


[DataType(DataType.EmailAddress)]
public string Email { get; set; }

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

Ha csak megjeleníteni szeretnénk, akkor egy email


linket generál belőle az MVC framework (a
szövegmező mellett jobbra látható kisbetűs email
cím). Ha viszont szerkeszteni szeretnénk, akkor
böngésző megköveteli, hogy valódi email címet írjunk be. Ez látható a hibás email címmel a baloldalon.

A DataType.MultilineText hatása, hogy


többsoros szöveges mező fog megjelenni.

A DataType.Password hatása, hogy jelszó beviteli mezőt kapunk.

A DataType.Date eredménye egy kulturált dátum beviteli mező,


amiben háromféle módon is megadhatjuk az értéket. Ráadásul
még a várt formátumot is jelzi számunkra. A naptár jellegű
kezelésről javascript kód gondoskodik.

Még számos további megjelenítést tud eredményezni a használata,


amit hamarosan a Html helpereknél fogunk részletesen megnézni.

DisplayFormatAttribute

A megjelenő adat formátumát határozza meg a DataFormatString tulajdonságában megadott string


formázók alapján. Az előző DisplayType attribútum is meghatároz formázást a különböző típusokhoz,
de ezzel az attribútummal azt is felül tudjuk bírálni. Az alapértelmezett viselkedése, hogy a
szerkesztőmezőkhöz nem határoz meg formázást, de ezt is ki tudjuk kényszeríteni az
ApplyFormatInEditMode = true beállítással. Lentebb az TotalSum propertyre azt határoztam meg,
hogy a számértéke pénznem formátumban jelenjen meg. Az LastPurchaseDate dátum+idő típusú
pedig csak a dátumot mutatja és fogadja.

[Display(Name = "Vásárlások összértéke")]


[DisplayFormat(DataFormatString = "{0:C}", ApplyFormatInEditMode = true)]
public decimal TotalSum { get; set; }

[Display(Name = "Utolsó vásárlás")]


[DisplayFormat(DataFormatString = "{0:d}", ApplyFormatInEditMode = true)]
public DateTime LastPurchaseDate { get; set; }

Az eredménye egy ilyen oldalrészlet. A bal oldalon


szerkesztőmezők, ezek mellett jobbra csak megjelenítés.
Mindkettőre kihat.

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.

4.3.2. Validáció attribútumokkal

A validációs attribútumok csak segédeszközök az adatérvényesítés problémájára, leginkább az adat


formátumára, értékhatárára, hiányára korlátozódva. Most csak egy felsorolásban végigmegyünk az
attribútumokon, és majd a 9.5 Validálás fejezetben fogunk foglalkozni az adat érvényesítés
részleteivel. A használatuk annyira egyszerű és automatikus, hogy szinte semmi magyarázatot nem
igényelnek. Rárakjuk egy modell propertyre és onnantól az MVC figyelni fogja, hogy a felhasználói
felületen bevitt adat megfelel-e az attribútum által lefektetett szabálynak. A ValidationAttribute közös
ősből származnak ezek az attribútumok és az alábbi táblázatban soroltam fel. A szövegbezúzás a
származási hierarchiát jelenti.

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(ErrorMessage = "A név megadása kötelező ({0})!")]

A másik az erőforrás ([ResourceFájlNév].resx) fájl használata.

[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

[StringLength(10, MinimumLength = 9)]


public string FullName { get; set; }

Az első paramétere a maximális szöveghosszat jelenti. A MinimumLength opcionális és mint a neve is


mutatja a szöveg minimális hosszát jelenti. A fenti esetben a FullName property csak 9 vagy 10 karakter
hosszú szöveget fogad el.

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; }

Alaphelyzetben integer és double értéket lehet megadni az elvárt értéktartomány kikényszerítésére. A


példában nyilvánvalóan nem adhatok meg a felületen 100-at az „TotalSum” property értékének.
Meghatározhatunk adattípust is, feltéve, hogy az adattípus egyértelműen konvertálható stringből,
mivel ilyenkor szöveges formában kell megadni a szélsőértékeket. Ezen kívül az adattípussal
kapcsolatban értelmezhetőnek kell lennie a kisebb-nagyobb relációnak. Lehet használni akár DateTime
típussal is, aminek a gyakorlati felhasználási lehetősége elég szűkös, mivel az attribútumok
paraméterének fordítás időben meghatározottnak kell lennie. A valós helyzetekben a dátum validáció
az esetek jó részében az adott naptól számított relatív szélsőértékeket használ (a mai napnál régebbi
vagy újabb, egy hónapnál nem régebbi, stb.), dinamikus értéket pedig nem tudunk adni. Majd később
látni fogjuk, hogy meg van a lehetőségünk, hogy új dinamikus dátum értékeket validáljuk.

[Range(typeof(DateTime), "2010.01.01", "9999.12.31")]

DataTypeAttribute

Ezzel már találkoztunk a megjelenítésre szakosodott attribútumok csoportjában, és említettem hogy


ez validációs funkciójú is. De csak akkor, ha a származtatott attribútum verzióját használjuk! Szó volt
arról, hogy a megjelenítését felül lehet bírálni a DisplayFormatAttribute-al. Ennek nem kívánt
4.3 Modell - Modell és jellemzők 1-57

mellékhatásai lehetnek, ha meggondolatlanul, egymásnak ellentmondóan állítom be a két


attribútumot. Például az adat típus csak idő, a megjelenítési formátum csak dátum:

[Display(Name = "Utolsó vásárlás")]


[DataType(DataType.Time)]
[DisplayFormat(DataFormatString = "{0:d}", ApplyFormatInEditMode = true)]
public DateTime LastPurchaseDate { get; set; }

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:

<input class="text-box single-line" id="Email" name="Email" type="email" value="proba@proba.hu" />

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ő.

EnumDataTypeAttribute, CreditCardAttribute, EmailAddressAttribute, FileExtensionsAttribute, PhoneAttribute,


UrlAttribute

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.

Nézzük is meg az EnumDataType felhasználását és ennek anomáliáit.


4.3 Modell - Modell és jellemzők 1-58

[Display(Name = "Vásárló típus")]


[EnumDataType(typeof(CustomerTypeEnum))]
public CustomerTypeEnum CustomerType { get; set; }

A példához tartozik egy enum definíció:

public enum CustomerTypeEnum


{
[Description("Nem ismert")]
Unknown,
[Description("Magánszemély")]
Person,
[Description("Kiskereskedő")]
Retailer,
[Description("Nagykereskedő")]
Supplier
}

Ennek a megjelenítése sajnos csak ennyi:

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; }

A ConfirmPassword tartalmát összehasonlítja a NewPassword tartalmával. Látszik, hogy a másik


property nevét szöveges formában kell megadni az attribútum konstruktor paraméterében. A
CompareAttribute-ot a .Net 4.5 –ös verziójához is soroltam, mert attól a verziótól fogva a
4.3 Modell - Modell és jellemzők 1-59

System.ComponentModel.DataAnnotations névtérben érhető el. A .Net 4.0 alatt viszont a


System.Web.Mvc névtérben lehet megtalálni. Emiatt egy 4.0 -> 4.5 verzióváltás esetén némi plusz
munkát jelent a névterek pontosítása. Az MVC 5-ben a System.Web.Mvc névtér alatti verziót
elavultnak jelölték (obsolete), tehát kerüljük a használatát.

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

Ez az attribútum, a kódból egyedileg megfogalmazott validációt segíti. A validálandó property legyen


megint a FullName, ami nem tartalmazhat számot. Tegyük fel, hogy most viszont nem szeretnénk erre
egy regex kifejezést írni, hanem favágó módszerrel esünk neki.

[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.

public static ValidationResult ValidateFullName(string fullName)


{
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;
}

A ValidateFullName a ValidationResult-ba csomagolt hibaüzenettel tér vissza, ha a "fullName" üres


vagy számot tartalmaz. Megfelelő "fullName" esetén, a Success statikus tulajdonságban elérhető
„hibátlan validáció” értelmű objektummal válaszol. Ezzel a módszerrel teljesen egyedi, property szintű
validációt tudunk definiálni.

Szemben az eddigi validátor attribútumokkal, ez használható osztályszintű validátorként is.


Megoldhatunk ezzel olyan adatellenőrzést, amikor több tulajdonság tartalmának kell konzisztensnek
lennie, együttesen értelmezhetőnek, érvényesnek vagy érvénytelennek. Ilyen szituáció lehet, ha nem
kötelező megadni a címet és az email címet egyszerre, elég ha az egyik kitöltésre kerül. Ezt property
szinten csak körülményesen, a CustomValidation-al viszont könnyen tudjuk érvényesíteni. Díszítsük ki
vele az osztályt és implementáljuk a hozzá tartozó metódust.

[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).

A hibás validáció eredménye is eltér az eddigiektől, mivel


nem köthető propertyhez, ezért a beviteli mezők felett
jelenik meg. És ha több validáció is elhasalna, akkor azok is itt
lennének felsorolva, piros gombóccal felvezetve. A
felhasználónévben továbbra sem szabad számot megadni. A
ValidationResult-ban megvan a lehetőség, hogy egy vagy akár
több inputmezőhöz rendeljük a megadott üzenetet. Ehhez
még a MemberNames nevű és IEnumerable<string> típusú
paraméterét kell kitölteni. Az alábbi példában egy string
elemtípusú tömböt adtam át, paraméterként:

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

A validációs attribútum példák egyben

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; }

[Display(Name = "FullNameLabel", ResourceType = typeof(Resources.UILabels))]


[Required(ErrorMessage = "A név megadása kötelező (1)!")]
//[Required(ErrorMessageResourceName = "UserNameRule",
// ErrorMessageResourceType = typeof(Resources.Validations))]
//[StringLength(10, MinimumLength = 9)]
//[DataType(DataType.Password)]
//[DataType(DataType.Url)]
[CustomValidation(typeof(ValidationDemoModel), "ValidateFullName")]
//[RegularExpression(@"^[a-zA-Z'\s]{1,20}$",
// ErrorMessage = "Kötelezően csak az angol ABC betűi lehetnek, maximálisan 20 karakter
hosszúságban!")]
public string FullName { get; set; }

[Display(Name = "Vásárló címe")]


[DataType(DataType.MultilineText)]
//[Required(ErrorMessageResourceName = "CimRule",
// ErrorMessageResourceType = typeof(Resources.Validations))]
public string Address { get; set; }

[Display(Name = "Vásárló email")]


[DataType(DataType.EmailAddress)]
//Dotnet 4.5: [EmailAddressAttribute]
public string Email { get; set; }

[Display(Name = "Vásárlások összértéke")]


[DisplayFormat(DataFormatString = "{0:g}", ApplyFormatInEditMode = true)]
//[Range(100.1, 200.1)]
public decimal TotalSum { get; set; }

[Display(Name = "Utolsó vásárlás")]


[DataType(DataType.Date)]
[Range(typeof(DateTime),"2010.01.01","9999.12.31")]
//[DataType(DataType.Time)]
[DisplayFormat(DataFormatString = "{0:d}", ApplyFormatInEditMode = true)]
public DateTime LastPurchaseDate { get; set; }

[Display(Name = "Vásárló típus")]


[EnumDataType(typeof(CustomerTypeEnum))]
public CustomerTypeEnum CustomerType { get; set; }

public static ValidationDemoModel GetModell(int id)


{
if (datalist == null) datalist = new Dictionary<int, ValidationDemoModel>();

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];
}

private static Dictionary<int, ValidationDemoModel> datalist;

public static ValidationResult ValidateFullName(string fullName)


{
4.4 Modell - Modell és tárolás. Adatperzisztencia 1-63

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;
}

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!",
new[] { "Address" });
return ValidationResult.Success;
}

public enum CustomerTypeEnum


{
[Description("Nem ismert")]
Unknown,
[Description("Magán személy")]
Person,
[Description("Kiskereskedő")]
Retailer,
[Description("Nagykereskedő")]
Supplier
}
}

4.4. Modell és tárolás. Adatperzisztencia

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

4.4.1. Adatbázis séma szerinti modellek

Ebben az esetben a modellosztályok


közel pontos megfelelői az adatbázis
táblák sémájának. A tábla minden
egyes mezője egyértelműen oda-
vissza megfeleltethető az
modellosztályunk propertyjeivel. A
modell éppúgy használható MVC
modellként, mint az adatelérési réteg
modelljeként. Erről szólt a 3.8 fejezet.
Ennek az előnye, hogy egy ORM-el
könnyen kezelhetően és típusosan
tudjuk az adatelérési réteget
megvalósítani.

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:

 automatikus entitás id generáltatás, tranzakció- és konkurenciakezelés


 csak relációban elérhető adatok
 esetleg szükségtelen teljes objektum gráfok. (minden tábla-osztály hozzáférhető). (Nem
lazy loading)

A tárolásközeli technikai adatok kezelésében és az előbbiekben említett állapotfüggő adatok


leválasztásában segítség lehet, ha beiktatunk még egy un. Repository (repository pattern21) réteget az
ORM felügyelt osztályok és a modell felhasználás közé, ami leválasztja az ORM-ről a
modellosztályainkat. Kisebb rendszerekben gondolkozva, ez a réteg elsőre feleslegesnek fog látszani
és többletmunkát is jelent, de érdemes ott tartani a tarsolyunkban a tudást, hogy van ilyen is.
Az előbb felsorolt három jellemzőből a két utolsó szokott problémát okozni és jó megoldást csak ügyes
tervezéssel lehet biztosítani. Két ilyen megközelítést említenék:
 Az egyik, hogy az objektumgráfot szétszedjük valóban szoros kapcsolatban levő csoportokra,
és ezt használjuk az alkalmazásunkban. A csoportok közti kapcsolatot pedig, manuálisan
feltöltött modellpéldányokkal tartjuk fenn. Ilyenkor egy modell csak egy csoportban szerepel.
Ezt azért elég nehéz megoldani és az adatbázis séma tervezésekor is figyelembe kell venni.
 A másik, hogy a csoportokat úgy alakítjuk ki, hogy egy-egy adathalmazigény számára csak a
minimálisan szükséges objektumokkal foglalkozzon. Ilyenkor egy-egy objektum több
kontextusban is tud szerepelni. Ez már nem függ annyira az adatbázis séma megvalósításától.

Ezt a metodikát multiple data contextnek


vagy bounded data contextnek szokták
nevezni. Ezt szemlélteti az ábra.
Példaként: ha az oldalunk a beszerzések
kezelésével foglalkozik, a „Beszerzés”
kontextust használjuk mivel biztosan nincs
szükségünk az Alkalmazottak (területi
képviselők) HR adataira.

21
http://msdn.microsoft.com/en-us/library/ff649690.aspx
4.4 Modell - Modell és tárolás. Adatperzisztencia 1-66

4.4.2. Nézet jellegű modell

Ilyenkor a modell feladata, hogy a View-t


és az actiont szolgálja ki. Más feladata
nincs, emiatt csak az MVC projektben van
csak értelme használni. Az kicsit túlzás
lenne, hogy minden egyes View számára
külön modellt definiálunk, ezért a
takarékosság miatt belekerülhet annyi
property és validátor, amely több View
számára is megfelel. Így nem kell külön
modell a listázó (táblázatot létrehozó),
szerkesztő (inputmezőket tartalmazó
form), részletes megjelenítő (detail
nézet, minden adattal) oldalakat
12. ábra generáló View-k számára.

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.

public class LocalPasswordModel


{
public string OldPassword { get; set; }

public string NewPassword { get; set; }


4.5 Modell - Az érinthetetlen, generált modell problémája 1-67

public string ConfirmPassword { get; set; }


}

Szolgáltatás (service) szerinti objektum modellek:

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.

4.5. Az érinthetetlen, generált modell problémája

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 Person osztályt automatikusan generáltatjuk, tehát "érinthetetlen". Most az áttekinthetőség


kedvéért, csak egy tulajdonsággal:

public partial class Person {


public string FirstName {get;set;}
}

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 {
}

public class PersonMetadata {

[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.

4.6. Egyéb modellattribútumok

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.

EditableAttribute, ReadOnlyAttribute, BindAttribute

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ó.

4.7. Egy demó modell

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; }

[Display(Name = "FullNameLabel", ResourceType = typeof(Resources.UILabels))]


public string FullName { get; set; }

//[AllowHtml]
[Display(Name = "Vásárló címe")]
//[DataType(DataType.MultilineText)]
public string Address { get; set; }

[Display(Name = "Vásárló email")]


[DataType(DataType.EmailAddress)]
//Dotnet 4.5: [EmailAddressAttribute]
public string Email { get; set; }

[Display(Name = "Vásárlások összértéke")]


public decimal TotalSum { get; set; }

[Display(Name = "Utolsó vásárlás")]


[DisplayFormat(DataFormatString = "{0:d}", ApplyFormatInEditMode = true)]
public DateTime LastPurchaseDate { get; set; }

[Display(Name = "Vásárlások listája")]


public IList<ActionDemoProductModel> PurchasesList { get; set; }

[Display(Name = "Kiemelt várárlás")]


public ActionDemoProductModel KeyPurchase { get; set; }

public int[] KeyPurchaseIds { get; set; }

[Display(Name = "Fontos ügyfél")]


public bool VIP { get; set; }

#region In memory perzisztencia


public static ActionDemoModel GetModell(int id)
{
if (datalist == null) datalist = new Dictionary<int, ActionDemoModel>();

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];
}

public static IList<ActionDemoModel> GetList()


{
return datalist.Select(dl => dl.Value).ToList();
}

public SelectList GetSelectList()


{
return new SelectList(this.PurchasesList, "Id", "ProductName",this.KeyPurchase.Id);
}

private static Dictionary<int, ActionDemoModel> datalist;


#endregion

public class ActionDemoProductModel


{
[HiddenInput(DisplayValue = false)]
public int Id { get; set; }

[Display(Name = "Cikkszám")]
public string ItemNo { get; set; }

[Display(Name = "Termék név")]


public string ProductName { 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 MVC kódkörnyezetéről szóló téma bevezetéseként szeretnék rávilágítani, hogy a kontroller és a


benne levő kódok laza kapcsolatban vannak a kódot indító eseményekkel. Ezt a kapcsolatot külön kell
deklarálni és ez elég dinamikus tud lenni. A lefutó action metódus kiválasztása egy sor deklaratívan
meghatározott szabályok együttes eredményeként történik meg. Ellenpéldaként említeném az
ASP.NET Web Forms alkalmazás alapértelmezett működését. Ott a code-behindban implementált kód
indítása, a kódhoz tartozó aspx oldal lekéréséből fakad. A másik ellenpélda a Winforms alkalmazás,
ahol egy ablak megnyitását gombok és menüelemek eseménykezelőiben futó kód végzi el.

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!

5.1. Az alkalmazásunk beállítása. A web.config

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:

c:\Users\[a Windows felhasználó név]\Documents\IISExpress\config\

É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>

Az kiemelt 'MvcApplication' a demóalkalmazás neve, ami a könyvben szereplő példákat tartalmazza. A


fejlesztés során a leghasznosabb sorát, a bindings listát szintén kiemeltem. Az első 'binding' a normál
beállítás. A következő sorral el lehet érni, hogy az IIS Express a gépünkön kívülről jövő kéréseket is
kiszolgálja a megadott IP címen ( ami a gépem IP címe volt éppen). Az adott porthoz a tűzfalat is ki kell
nyitni.

A következő a konfigurációs láncolatban a machine.config–gal azonos mappában levő web.config fájl.


Ebben már nagyon sok beállítást találunk. Például ami részletesen meghatározza, hogy a különböző
kiterjesztésű fájlok kiszolgálása/feldolgozása melyik szerver modul feladata legyen.

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.

5.2. Az alkalmazás kiindulási pontja. A global.asax

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

az MVC rendesen működjön szükséges annak inicializálása az Application_Start nevű


esemény/metódusban. Ez a metódus az alkalmazás indulásakor fut le, amit az első request beérkezése
indikál. A további requestek esetén már nem fog lefutni.

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_BeginRequest()* – A request megérkezik az alkalmazáshoz. Mielőtt bármi is foglalkozott


volna vele.

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.

Application_PostAuthenticateRequest() – Hitelesítés után

Application_AuthorizeRequest() – A felhasználó hitelesítése után következik. Itt lehet a szerep alapú


jogosultságokat és magukat a szerepeket beállítani.

Application_ResolveRequestCache() – Az előtt fut le, mielőtt az oldal kiszolgálása megtörténne a


cache-ben tárolt oldalváltozat alapján. (OutputCache)

Application_AcquireRequestState() – A Session adatok feltöltése előtt.

Application_PreRequestHandlerExecute() – Mielőtt a normál oldalgenerálás/oldalkiszolgálás


elindulna.

Application_PostRequestHandlerExecute() – A oldalgenerálás után.

Application_ReleaseRequestState() – A request objektum utolsó állomása. Ekkor még tudjuk kezelni


a Session-t is.

Application_UpdateRequestCache() – Mielőtt a cache-be kerülhetne a generált HTML (vagy egyéb)


kimeneti eredmény.

Application_EndRequest()* – Mindennek a vége.

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.

Application_Start()* – Erről volt szó az előbb. Az alkalmazás indulásakor fut le.

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.

Application_Error()* – Alkalmazás szintű hiba.

Session_End() – Lejárt vagy eldobott session objektum.

Application_End() – Az alkalmazás futásának a végén indul el. Bekövetkezik, ha manuálisan állítjuk le


a webszervert, vagy ha az application pool ideje lejárt. Például a saját naplózási rendszerünk számára
egy lezáró sort lehet kiküldeni.
5.2 A kontroller és környezete - Az alkalmazás kiindulási pontja. A global.asax 1-75

Application_Disposed() – Az utolsó lehetőség, hogy a saját, alkalmazás szintű erőforrásokat mi is


lezárjuk.

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.

Nézzük meg, hogy néz ki egy MVC inicializálás:


public class MvcApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();

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).

(Jobb gombbal az ikonra kattintva és ’Exit’)

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

közben. Ezek az alkalmazásleállások abból következnek, hogy a webszerver monitorozza a futáshoz


szükséges legfontosabb fájlokat így a web.config-ot és természetesen az alkalmazás dll fájljait. Ha ezek
megváltoznak, akkor az alkalmazás életben tartása értelmét vesztette, és le is állítja azt. Ez egy sokkal
jobb viselkedés, mintha az alkalmazást manuálisan kellene leállítani, odamásolni és újraindítani
minden fordítás során, legalábbis a mi szempontunkból. Egyet azonban jegyezzünk meg: éles
helyzetben futó alkalmazásnál ne próbáljuk meg felülírni a dll-jeit manuálisan, mert csúnyán
megtréfálhat minket az IIS fájlmonitorozó képessége.
5.3 A kontroller és környezete - Routing 1-77

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.

public class RouteConfig


{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

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:

public ActionResult Contact(string id)

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.

Még nem volt szó a RegisterRoutes első soráról:

routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

Mint a neve is mondja ez egy kivétel, azaz ha az URL mintája ráilleszthető a


„{resource}.axd/{*pathInfo*}” szabályra, akkor azt az MVC visszadobja, hogy foglalkozzon vele inkább
az ASP.NET motor. Az ASP.NET alatt az .axd kiterjesztésű fájlok - az un. HTTP handlerek - egy lefordított
kódban állítják össze a requestnek megfelelő teljes response csomagot. Például a paramétereknek
megfelelő képet.

Route mapping saját célra

Bővítsük a route bejegyzéseket egy új elemmel, „Kategoriak” néven.

public static void RegisterRoutes(RouteCollection routes)


{
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:

public ActionResult Index(string category, string id)


{
ViewBag.Message = String.Format("Kategória: {0} Id: {1}", category, id);
return View();
}
Ha ezek megvannak, akkor a /Home/Index/Butorok/12 URL végződéssel (URL path) megnyitott
oldalunk fejléce így fog kinézni:
5.3 A kontroller és környezete - Routing 1-79

A ViewBag.Message tartalmát a Views/Home/Index.cshtml elején jeleníti meg ez a sor:

<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}".

Ehhez egy ilyen URL passzol: /Webshop/Home/Index/Vilagitas/16.

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.

Nézzük a két route eredményét, két hozzáillő URL-el:

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

defaults: new { controller = "Termekek", …)

és megvalósítjuk a TermekekController-t, akkor jó úton járunk, hogy a route konfigurálás erejét ki


tudjuk használni.

Végső ellenpróbaképpen adjuk meg Url-nek a következőt: /Home/Index/Butorok/101. Az eredmény


egy 404-es "oldal nem található" hibaüzenet lesz, mivel erre az URL-re nem tudott egy route bejegyzést
sem illeszteni. Erről azt érdemes megjegyezni, hogy a ’/’ jelekkel elválasztott URL-t addig tudjuk
bővíteni, ameddig megírjuk rá a megfelelő route bejegyzést. Közmondásosan: addig nyújtózkodjon az
URL-ed, amíg a route takaród ér.

{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

/webshop/Home/Index?category=Vilagitas&id=16 azonos eredményt kapunk mintha a

/webshop/Home/Index/Vilagitas/16 –t írtuk volna.

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:

public ActionResult Index(string category, string id)

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ó:

public ActionResult Index(string category, int id)

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

public class MultiConstraint : IRouteConstraint


{
public bool Match(HttpContextBase httpContext, Route route, string paramName,
RouteValueDictionary valuesDict, RouteDirection routeDirection)
{
object idobject;
if (!valuesDict.TryGetValue("id", out idobject) || idobject == null)
return false;
int id;
if (!Int32.TryParse(idobject.ToString(), out id))
return false;
if (id < 1 && id > 10000)
return false;

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.

Az IRouteConstraint példa felhasználása hasonló a regular expression-ös változathoz. A paraméter


nevénél az "id_akarmi"-vel azt szerettem volna jelezni, hogy ilyen esetben lényegtelen a paraméter
neve mivel úgysem azt vizsgáljuk (de azért megérkezik a Mach metódusba paramName). Mivel az
értékeket mind megkapjuk a "valuesDict" nevű paraméterben.

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

Fájl alapú route

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:

URL Fájl elérési úttal


/forms/One ~/AspPages/One.aspx
/forms/Two ~/AspPages/Two.aspx
/forms/Three ~/AspPages/Three.aspx

Ezt lefedi ez a metódushívás a paramétereivel:

routes.MapPageRoute("staticPages", "forms/{webform}", "~/AspPages/{webform}.aspx");

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:

Funkcionalitás alapú route minta Kontroller alapú route minta


/popupmenu/product/1 /product/popupmenu/1
/popupmenu/categories/2 /categories/popupmenu/2
/popupmenu/rates /rates/popupmenu
5.3 A kontroller és környezete - Routing 1-84

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)

HttpGetAttribute(string routeTemplate), HttpPostAttribute(string routeTemplate), stb.

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.

[AcceptVerbs(HttpVerbs.Get | HttpVerbs.Post, RouteTemplate = "RC/Name1")]


public ActionResult Details1() {
return View();
}

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();
}

A következő példában kiegészítettem egy további paraméter szekcióval, aminek ráadásul az


alapértelmezett értékét is megadtam. (defaulvalue=Alapertek)

[HttpGet("categories/{categ}/Details/{id:int}/{defaulvalue=Alapertek}/{notanoption}",
RouteName = "Details5Route")]
public ActionResult Details5(string categ, int id, string defaulvalue, string notanoption)
{
return View();
}

A 'notanoption' viszont egy kötelező, típusmeghatározás nélküli paraméter. A RouteName


paraméterrel megadhatjuk a route bejegyzés nevét is. Ellenkező esetben a route minta lenne a neve,
amire elég nehéz hivatkozni. Sokkal egyszerűbb, ahogy a második sorban van:

@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

Inline route megkötések.

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.

Érték alapú A vizsgálat módja


bool Értelmezhető-e: booleanként? (parsolható booleanra?)
datetime dátumként?
decimal decimális értékként?
double doubleként?
float floatként?
guid guidként?
int egész számként?
long 64bites számként?
Karakterszám
minlength(x) A route szakasz minimális hossza karakterekben.
maxlength(x) A route szakasz maximális hossza karakterekben.
length(l,h) A route szakasz pontos hossza karakterekben.
Érték
min(x) A számként értelmezhető érték minimuma.
max(x) A számként értelmezhető érték maximuma.
range(l,h) A számként értelmezhető érték tartománya.
Egyedi forma
alpha Csak betűk lehetnek.
regex(…) Reguláris kifejezés kiértékelése szerint.

Íme, néhány példa:

A 'categ' helyén érkező URL szakasznak minimálisan 10 karakter hosszúnak kell lennie.

[HttpGet("categories/{categ:minlength(10)}/Details/{id:int}", RouteName = "Details7Route")]


public ActionResult Details7(string categ, int id)
{
return new ContentResult() { Content = "minlength(10)" };
}

Ha nincsenek ellentmondásban, akkor lehet láncolni is a megkötéseket kettősponttal elválasztva. Az


alábbi példában az 'id' helyén minimum egy 10-es számnak kell állnia:

[HttpGet("categories/{categ}/Details/{id:int:min(10)}", RouteName = "Details8Route")]


public ActionResult Details8(string categ, int id)
{
return new ContentResult() { Content = "láncolt : minlength(10):alpha , nem megy" };
}

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"

Fontos megjegyezni, hogy a RoutePrefixAttribute nem befolyásolja a hagyományos route szolgáltatást.


A normál 'Default' kontroller/action séma is működik (amíg ki nem töröljük). Ezért az
RouteDemo/RC/Name1 mellett még használható a /RoutingAttr/Details1 útvonal is. Továbbá nem
határozza meg a normál (route attribútum nélküli) actionök elérését. Így a /RoutingAttr/Index működni
fog, de a RouteDemo/Index nem, ha az Index metódust érintetlenül hagyjuk.

É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.

public static void RegisterRoutes(RouteCollection routes)


{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

var controllerTypes = new[] { typeof(RoutingAttrController) };


routes.MapMvcAttributeRoutes(controllerTypes);

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

A mi általunk megvalósítható kontrollernek a Controller ősosztályból kell származnia. Az osztályunk


elnevezése kötött, mert a route bejegyzés …{controller}/… szakasz nevével kell kezdődnie és a
’Controller’ szóval záródnia. Amikor az MVC framework megkapja a kérést, annak URL-jéből a route
bejegyzések alapján meghatározza a kontroller nevét, majd megpróbálja megkeresni és
példányosítani. Itt kicsit bajba kerülhetünk, ugyanis az MVC-nek tényleg csak az osztálynév számít
normál route konfiguráció mellett. Emiatt, ha definiálunk két HomeController-t más névtérben, a C#-
nak nem fog problémát okozni, és a kódunk fordítható lesz. Viszont az MVC nem fogja tudni
megmondani, hogy melyikre gondoltunk. Ez kis alkalmazásnál nem jelent gondot, mert miért is
csinálnánk két HomeController-t. Ahogy azonban nő az alkalmazásunk, kontroller névütközések is
felléphetnek. Olyan kontrollerneveket mégsem adhatunk, hogy
„AdminSzekcioElsodlegesNemMobilHomeController”. A sokkontrolleres problémára van megoldás. Az
’Area’, a funkcionális csoportosítás lehetősége, de erről egy későbbi fejezet szól a könyv végén.

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.
}

Minden requesthez új kontrollert fog példányosítani az MVC, ebből következik, ha kilépünk a


meghívott action metódusból a kontroller példányunk már nem marad a hatáskörünkben, elvégezte a
feladatát. Majd a Garbage Collectorral kerül közelebbi viszonyba. A másik következmény, hogy nem
érdemes bődületesen nagy kontrollert készíteni. Gondoljuk bele, hogy egy request kiszolgálásához
általában kevés (1-2-3) action szokott kelleni, ezért nem érdemes példányosítani egy sokmetódusú
kontrollerosztályt, és csak az azonos adattémájú actionöket célszerű egy kontrollerbe helyezni. Az
olyan nagyobb segédmetódusokat, amelyeket az actionök közösen használnak, érdemes egy statikus
helper osztályba kitenni. Ez kicsit ellentmond néhány objektum orientált tervezési elvnek, de ilyen
esetben győzhet a pragmatizmus, és némi sebességelőnyhöz is jutunk.

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.

Másrészről viszont a statikus alkalmazásszintű értéklisták és metódusok, a statikus helper osztályok


(jellemzően ilyen névvel illetik a közösen használt statikus metódusok gyűjteményét) segítségünkre
szoktak lenni. Ezt nagyon sok esetben az MVC keretrendszer is így oldja meg. Mivel a kontroller az a
szekció, ahová a kódok jelentős része kerül, még egy figyelmeztetés ide kívánkozik. Ha a megelőző
5.4 A kontroller és környezete - Controller 1-89

fejlesztői tapasztalatunk desktop alkalmazások programozása volt, mondjuk némi szálkezeléssel és


párhuzamosan futó kóddal, akkor kicsit hátradőlve, becsukott szemmel képzeljük el, ahogy a
kontrollerünk metódusa reagál a beérkező kérésre és meghívja a statikus helper metódusunkat. Majd
reagál a következő böngészési kérésre, és még 100-ra egyszerre. Még néhány másodperc és a
beérkezett és kiszolgált kérések száma több ezer lehet. Tartsuk észben, hogy a kiszolgálói oldalra írt
programot nem egy felhasználó, és nem egy gépről fogja használni, hanem lehet, hogy
megszámlálhatatlanul sokan! Az erőforrások (pl. adatbázis vagy fájlművelet) kezelése esetén nagyon
körültekintően alkalmazzuk az erre vonatkozó szabályokat. Olyanokra gondolok, mint a kritikus
program szakaszok lock-olása, az erőforrások felszabadítása az IDisposable interface
figyelembevételével. A try+catch kivételkezeléskor gondoljuk meg, hogy nincs-e szükség esetleg a
finally blokkra is.

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

A controller néhány tulajdonsága

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:

Application - Az MVC esetében nem annyira számottevő HttpApplicationState típusú


Application objektumot ezen keresztül lehet elérni. Nagyjából arra jó ez a dictionary, hogy az
alkalmazás futása alatt megőrzendő közös adatokat tárolhatjuk benne. Az alkalmazás leállásával a
tartalma elveszik, viszont addig mindenhonnan elérhető ahol a HttpContext is elérhető. A
használatának erősen javasolt módja, hogy a beleírás előtt hívjuk meg a Lock() metódusát, majd az
írást követően az UnLock()-ot. (Többszálas alkalmazást futtatunk…).

Request – Az ASP.NET+MVC a böngészőtől érkező request adatait egy HttpRequestWrapper


objektumba kivonatolja. Ez pedig elérhető a controller példányunk Request tulajdonságán keresztül.
Ebben minden request adat rendelkezésünkre áll. Olyanok, mint a böngésző típusa, az URL, az URL-ből
a domain név:portszám és az utána levő URL path elkülönítve, a cookie, a feltöltött fájl(ok). Ott van a
query string, ami az URL-ben a ’?’ után szokott lenni, a szerver változói és sorolhatnám. Amit fontos
megjegyezni, hogy bármi, ami a HTTP protokollon szokott érkezni, azt itt kell keresni. Szerencsére az
MVC továbbmegy és az itt fellelhető információból action metódushívást és annak paramétereit fogja
képezni, ezért valószínűleg nem kell vele foglalkozni. Viszont, ha valami nem úgy működik, ahogy
elvárható lenne, pl. az action paramétere nem tartalmaz értéket, akkor hibakeresés céljából jó ha
tudjuk, hogy létezik a Request nyers valósága is. Mivel ennek a feldolgozása és értelmezése az MVC
kiemelt feladata, később számos helyen találkozunk még vele, sőt az egész 8. és 9. fejezet a request
feldolgozási folyamatáról szól.

Response – A Request a párja a Response, ami tartalmazza azt, ami a böngészőnek


visszaküldésre kerül. A HTML tartalmat, a HTTP állapot kódot, a kimenő cookie-kat, a HTTP fejlécet.
Ebben lehet meghatározni a karakterkódolást, a tartalom típus szabványos elnevezését (’ text/html’, ’
image/jpeg’, stb.), a tartalom elévülési idejét is. A Response objektumot még ritkában kell kezelni
programból, mivel az MVC feladata pont az, hogy ezt a válasz-adathalmazt szépen előállítsa magasabb
absztrakciós szinten, a View-k és ActionResult-ok alapján. A Response egy alacsony szintű intelligens
objektum számos metódust biztosít a HTTP válasz közvetlen írására. Ezt a műveletet szokták úgy is
nevezni, hogy "írni a responsba", "a repsonse kimenetére írni", stb.

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.

Server – Némi szerver környezeti információt tárol. Leginkább a MachineName tulajdonsága


szokott felhasználásra kerülni (több kiszolgálós környezetben), ami a webszervert futtató gép nevét
hordozza.

Session – A következő alfejezetben részletesen megnézzük.


5.4 A kontroller és környezete - Controller 1-91

User – Ha használunk hitelesítést, akkor a bejelentkezett felhasználó kivonatos adatait


tartalmazza. A tartalma attól függ, hogy milyen hitelesítést használunk. Egy egész fejezet tárgyalja
később a felhasználók kezelését.

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:

public ActionResult Index(string category, string id)


{
//Index alapján: Session[0] = 1;

Session["HomeItem"] = 10;
return View();
}

public ActionResult About()


{
int tiz = (int)Session["HomeItem"];
return View(tiz);
}

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:

public class HomeController : Controller


{
public ActionResult Index(string category, string id)
{
ViewBag.Message = String.Format("Kategória: {0} Id: {1}", category, id);
this.HomeSession.PreviousId = 10;

return View();
}

public ActionResult About()


{
ViewBag.Message = "Your app description page.";
int tiz = this.HomeSession.PreviousId;
Session.Abandon();
return View();
}

public HomeSessionData HomeSession


{
get
{
string sessionName = this.GetType().Name;
return (HomeSessionData)(Session[sessionName] ??
(Session[sessionName] = new HomeSessionData()));
}
}
}

[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

javaslat érhető el a Session leghatékonyabb kezeléséről. Remélem sikerült érzékeltetnem, hogy a


Session egyrészről nagyon hasznos tud lenni, másrészről a használata az átlagnál kicsivel több
odafigyelést igényel.

Néhány session használati szempont:

 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.

A Session beállítását a web.config-ban lehet megtenni a sessionState tulajdonsággal. Ez a kis részlet


kikapcsolja a teljes sessionkezelést.

<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

 InProc – Ez az alapértelmezett. A webszerver memóriájában tárol, processzenként elkülönítve.


 StateServer – Ezzel azt határozzuk meg, hogy a session adat egy másik szerver memóriájában,
az ASP.NET State Service szolgáltatás segítségével tárolódjon. Természetesen emiatt számos
további paramétert is be kell állítani, ami a másik gép elérését definiálja. Ilyenkor a
webszerverünk, vagy webalkalmazásunk újraindulása esetén a felhasználó session adata
megmarad. Így ha az újraindulás elég gyors, észre sem fogja venni, hogy valami történt. Az
újraindulási sessionvesztés kivédése mellett megvan a lehetőségünk, hogy több webkiszolgáló
és egy session State Service-t futtató gép esetén a webkiszolgálók között terheléselosztást
valósítsunk meg. A terheléselosztás egyik jellemzője, hogy nem biztos, hogy az azonos
felhasználótól érkező kéréséket mindig azonos szerver fogja kiszolgálni, ezért nincs értelme a
webszerver saját memóriájában tárolni a Session adatokat.
 SQLServer – StateServer-es megoldáshoz hasonlóan a session egy másik gépen tárolódik, de
ezzel a tárolás perzisztens, és MSSQL adatbázisban tárolódnak az adatok. Emiatt tovább
5.4 A kontroller és környezete - Controller 1-94

skálázható és még biztonságosabbá tehető a nagy forgalmú webalkalmazások session tárolása.


Szóba jöhet akkor is, ha a session adatokat extrém hosszú ideig (napok, hetek) kell eltárolni.
Természetesen ez a megoldás lassabb, mint az előző, de nem annyira, ha úgy állítjuk be az SQL
szervert, hogy a rendelkezésére álló jó sok memóriájában tárolja a session tábla adatait. Az
sqlConnectionString tulajdonságban kell meghatározni az SQL szerver elérését és
természetesen az SQL szerveren a tábla struktúrát és az alapadatokat is létre kell hozni.
 Custom – Ez a haladó változat, amikor egy általunk implementált providerrel ott tároljuk a
session adatokat, ahol akarjuk. Fájlban, memóriában. Fájlban, az egyik féle adatot,
memóriában a másik típusút, stb.
 Off – Nincs tárolás.

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ó:

<sessionState timeout="60" />

A global.asax-ban, azaz az alkalmazásunkban két esemény is bekövetkezik a sessionkezeléssel


kapcsolatban.

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.)

A hagyományos ASP.NET-es session beállítás mellett egyedileg, bármelyik MVC kontrollerünkön


használhatjuk a SessionStateAttribute attribútumot, amivel a Session működését tudjuk kicsit
szabályozni. A paramétere a SessionStateBehavior enumeráció:

SessionStateBehavior.Default – A normál web.config-ból jövő session-kezelés vonatkozik a


kontrollerre.

SessionStateBehavior.Disabled – A kontroller nem használ session-kezelést.

SessionStateBehavior.ReadOnly – A kontroller csak olvashatja a session bejegyzéseket, de nem tölthet


bele újat és a meglévőt sem írhatja á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

5.5. Action és paraméterei

A beérkező requesteket a route bejegyzések szerint a kontrollerek action metódusaihoz irányítja az


MVC framework. De milyen kontrollermetódus lehet egyáltalán action?

Minden publikus metódus,


 ami nem statikus,
 nincs a paraméterei között out vagy ref módon definiált elem,
 nem felülbírálása a controller ősosztály metódusának,
 nem igényel nyitott generikus típust, mint paramétert, (List<T>, Dictionary<T,string>).

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.

A 3. fejezetben látott módon hozzunk létre egy ActionDemo kontrollert.

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)

Ahhoz, hogy kitudjuk kipróbálni ezeket az


actionöket, készítsünk hozzá View-kat is.
Használjuk erre is a varázslót.

Jobb klikk a View() metódushívásra és a helyi menüből az Add


View…-t válasszuk. Kész is a View-nk. Ráadásul ott jött létre ahol
kell.
5.5 A kontroller és környezete - Action és paraméterei 1-96

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.

public ActionResult Details(int? id).

Í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ó.

Folytatva a próbálkozást, a C# lehetőséget ad a metódus paraméterek számára alapértelmezett érték


megadására. (int id = 0). Így is jó lesz, ha nem adunk meg az URL-ben értéket az Id számára.

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

A @Model az id értékét hordozza. Ha most megnézzük a Details oldalt (/ActionDemo/Details), akkor


egy ’0’-nak is meg kell jelennie. Próbáljuk ki /ActionDemo/Details/5, /ActionDemo/Details/{akármilyen
szám}. Meg kell jelennie a számnak, amit megadunk az URL-végén.

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

public ActionResult Details(int id = 0, string category = "nincs", string format = "nincs"])


{
ViewData["kategoria"] = category;
ViewData["formátum"] = format;
return View(id);
}

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"]

A böngészőben nálam ez jelent meg:

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.

Az URL-t módosítsuk így: ’/ActionDemo/Details/8?category=További&format=Rendezett’ és nézzük az


eredményt:

Tehát így is megy. Ebből következik, hogy az URL paraméterek is


megfeleltethetőek action metódusparamétereknek a nevük alapján.
Továbbgörgetve a példát lehetséges ilyen URL-t is használni:
/ActionDemo/Details?id=10&category=További&format=Rendezett
5.5 A kontroller és környezete - Action és paraméterei 1-98

Az id-t meg lehet adni így is úgy is.

É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:

public ActionResult Details(int id = 0)


{
ViewData["kategoria"] = Request["category"];
ViewData["formátum"] = Request["format"];
return View(id);
}

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

5.6. Az action kimenete, a View adatok

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

Ez egy érdekes dictionary. A ViewData-hoz hasonlóan ez is string indexű és az elemeinek típusa


objektum. Azonban a tartalma elérhető a teljes request feldolgozása alatt. Ebbe beleértendő a child
actionök és azok View-jai is. A további képessége, ha egy action metódus végén egy HTTP redirekció a
visszatérési érték (pl. RedirectToAction egy másik actionre), akkor az átirányított actionben és a hozzá
tartozó View-ban is elérhető marad. A tartalma a Session objektumba van beágyazva emiatt a session
elvesztésével ennek tartalma is törlődik.

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

protected void Application_BeginRequest(Object sender, EventArgs e) {}.

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.

ActionResult leszármazott Viselkedés Controller metódus név


ContentResult Szöveg kiküldése Content
EmptyResult Nincs kimenet
FileContentResult, Fájl tartalom küldése File
FilePathResult,
FileStreamResult
HttpUnauthorizedResult HTTP 403-as kód
JavaScriptResult Javascript fájl küldése JavaScript
JsonResult JSON adat küldése Json
RedirectResult Új URL-re irányítás Redirect
RedirectToRouteResult Új Actionhöz iránytás RedirectToRoute vagy
RedirectToAction
ViewResult View renderelése View
PartialViewResult View részlet renderelése PartialView

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.

public ActionResult GetEmptyResult()


{
//1. változat
return new EmptyResult();

//2. változat
return null;
}
5.7 A kontroller és környezete - ActionResult 1-102

ContentResult

Minden View-t mellőzve, a paraméterként kapott szöveget válaszként küldi a böngészőnek.

public ActionResult GetContentResult()


{
var txt = "Ez kerül ki a kimenetre.";

//1. változat, kontroller metódus


return Content(txt);

//2. változat
return new ContentResult() { Content = txt };

//3. változat
Response.Write(txt);
return null;
}

Ez a visszatérési típus az 1. változatban nagyon egyszerűen a megadott szöveget elküldi a böngészőnek


a controller példány Content metódusát hívva. A 2. példa nem hívja a controller helper metódust,
hanem közvetlenül példányosítja a ContentResult osztályt és adja meg a Content propertyben a
tartalmat. A 3. változatban pedig a szöveget közvetlenül a Response objektum Write metódusával
küldjük a böngészőnek. Valójában az 1. változat controller Content metódusa a 2. változat
megvalósítása szerint hozza létre a ContentResult-ot, ami ContentResult pedig a 3. változat
megvalósításával küldi ki a választ a böngészőnek. A 3. változat végén látszik, hogy lehet a visszatérési
érték null is. Ilyenkor a Response helyes és teljes összeállításáról nekünk kell gondoskodni. Ezzel csak
azt akartam szemléltetni, hogy a controller ActionResult leszármazottait előállító metódusai (Content,
Json, View, stb.) csak annyit csinálnak, hogy példányosítják a megfelelő ActionResult leszármazottat és
felparaméterezik. A ContentResult-nak létezik egy ContentType tulajdonsága is, amivel a HTTP
szabvány szerinti tartalom típust tudjuk megadni. Ez alapértelmezetten: 'text/html'.

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

FileContentResult, FilePathResult, FileStreamResult

Példakódok:

public ActionResult GetFileContentResult()


{
byte[] filedata = System.IO.File.ReadAllBytes(Server.MapPath("~/Images/heroAccent.png"));
FileContentResult filecontent = new FileContentResult(filedata, "image/PNG");
//Ha megadjuk 'download to file' lesz és nem megjelenítés a böngészőben.
filecontent.FileDownloadName = "heroAccent.png";
return filecontent;
}

public ActionResult GetFilePathResult()


{
FilePathResult filePathResult = new FilePathResult(
Server.MapPath("~/Images/heroAccent.png"), "image/PNG");
//filePathResult.FileDownloadName = "heroAccent.png";
return filePathResult;
}

public ActionResult GetFileStreamResult()


{
byte[] filedata = System.IO.File.ReadAllBytes(Server.MapPath("~/Images/heroAccent.png"));
System.IO.MemoryStream ms=new MemoryStream(filedata);
FileStreamResult fileStreamResult = new FileStreamResult(ms, "image/PNG");
//fileStreamResult.FileDownloadName = "heroAccent.png";
return fileStreamResult;
}
11. példakód

A fenti kódban a három Action bemutatja a három visszatérési típust.

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

A JSON adatot két HTTP metódusban is lehet kérni, GET és POST.

A JsonRequestBehavior = JsonRequestBehavior.AllowGet – beállítással engedélyezhetjük a get HTTP


metóduson keresztüli kérést, mert alapértelmezetten, biztonsági okokból le van tiltva.

public ActionResult GetJsonResult()


{
var data = new JsonModelClass() { Id = 1, FullName = "Pista" };
return new JsonResult() { Data = data,
JsonRequestBehavior = JsonRequestBehavior.AllowGet };
}

public class JsonModelClass


{
public int Id { get; set; }
public string FullName { get; set; }
}

Az Action eredménye a böngébőben: {"Id":1,"FullName":"Pista"}

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.

Természetesen, a közvetlen JsonResult példányosítás helyett használhattam volna a controller Json


metódusát is: return Json(data);

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:

return new JsonResult() { Data = new { Id =1, FullName =”Pista”} };

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.

public ActionResult GetHttpUnauthorizedResult()


{
return new HttpUnauthorizedResult();
}

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.

public ActionResult GetRedirectResult()


{
return new RedirectResult("http://index.hu", true);
}

public ActionResult GetRedirectToActon()


{
//1. változat
return RedirectToAction("GetContentResult", "ActionDemo");

//2. változat
return new RedirectToRouteResult("Default",
new RouteValueDictionary(new { action = "GetContentResult", controller = "ActionDemo" }));
}

A RedirectResult második konstruktorparamétere a ’permanent’, amivel közölhetjük a böngészővel (és


a kereső motorokkal), hogy az átirányítás végleges, a kért oldal megszűnt, ha tehetik, az új URL-t
használják ezek után. Ekkor a HTTP válaszkód 301-es lesz.

A GetRedirectToAction action példa érdekesebb, ugyanis a határozott URL helyett Action/Controller


nevet lehet megadni. Az előző példában az URL-nek adhattam volna relatív útvonalat is, mint például:

return new RedirectResult("GetContentResult", false);

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:

public class ActionDemoController : Controller


{
//
// GET: /ActionDemo/
public ActionResult Index()
{
//1. változat
return View();

var model = new List<int>();

//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");

return new ViewResult() {};


}
}

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.

A második változatban modellt is adunk át a View-nak. A harmadik változatban meghatározzuk a View


nevét (még mindig index.cshtml), tehát már nem az aktuális metódus neve lesz az. A negyedik
metódusban még a Layout template fájl nevét is meghatároztam a masterName paraméteren
keresztül. Az ötödik változat is egy lehetőség. Ezzel kapcsolatban megjegyzendő, hogy a harmadik
változatban a modell típusa nem lehet csak egy string, mert akkor az az ötödik változatot jelenti, és a
modellünket layout névnek fogja értelmezni. Ezen felül természetesen kezünkbe vehetjük teljesen az
irányítást és egy ViewResult példányt úgy paraméterezünk, ahogy akarunk. Ennek van néhány
fontosabb kiegészítő lehetősége a View() controller metódushoz képest:

 Közvetlenül beállíthatjuk a ViewData, ViewBag, TempData tárolókat, a kontroller által


biztosítottak helyett. Ebből következik, hogyha a View() kontroller metódust használjuk, akkor
a kontroller ViewData, ViewBag, TempData objektum referenciái a háttérben a ViewResult-ba
másolódnak a ViewResult feldolgozása előtt.
 Megadhatunk más View Engine-eket (amik a View-t szövegesen értelmezik és feldolgozzák)
 Megadhatunk egyedi IView-t megvalósító osztályt, aminek az egyetlen követelménye, hogy
legyen egy Render metódusa, ami a View (vagy akármi) szöveges kimenetét kell, hogy
legenerálja. Ez hasonló az ASP.NET ServerControl Render feladatához. Így View template
(cshtml, vbhtml) nélküli, egyedileg kódolt oldalgenerálást valósíthatunk meg.

A ViewResult feltöltése és a Controller.View(modell) változat nélkül úgy is át tudjuk adni a modellt, ha


a ViewData.Model propertybe töltjük bele azt közvetlenül.
5.7 A kontroller és környezete - ActionResult 1-107

public ActionResult Test(int? id)


{
ViewData.Model = new TestModell();
return View();
}

PartialViewResult

Ez a részleges View-k (PartialView 6.4 fejezet) rendereléséhez készült, ActionResult leszármazott. A


ViewResult-hoz képest abban tér el, hogy nem lehet megadni masterName paramétert, hiszen a partial
View-k nem használják a layout templateket. A PartialView-k az őt használó View-ba ágyazódik bele,
úgy is fogalmazhatjuk a Partial View mester oldala maga a beágyazó View fájl. Ezen kívül más a View
mappa meghatározása, ami jelenleg még nem fontos számunkra.
5.8 A kontroller és környezete - Action kiválasztása 1-108

5.8. Action kiválasztása

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()) {

@Html.HiddenFor(model => model.Id)

<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.

Ez a HttpPost attribútum egy ActionMethodSelectorAttribute leszármazott. A legfontosabb HTTP


method-ok számára van egy-egy ilyen attribútumunk:

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.

HttpGet + azonosító – Adat megnyitása

HttpPost + azonosító – Adat mentése

HttpPut – Új adat létrehozása

HttpDelete + azonosító – Adat törlése

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) { }

//Értelmes action nevek. Friendly URL jellegű elnevezések.


[HttpGet]
public ActionResult EditProduct(int id) { }

[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.

[AcceptVerbs(HttpVerbs.Get | HttpVerbs.Put | HttpVerbs.Head)]


public ActionResult Edit(int id)
{
return View(ActionDemoModel.GetModell(id));
}

Az azonos nevű actionök paramétereinek a meghatározásakor a nyelvi szabályokra továbbra is


figyelemmel kell lenni, mert két azonos szignatúrájú metódus továbbra sem lehet egy osztályban.
5.9 A kontroller és környezete - Filterek 1-110

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.

Az ennél sokkal gyakrabban használt ActionNameAttribute segítségével a metódusunknak álnevet


(alias) tudunk adni. Ezzel az Action keresésekor nem az action metódusunk nevét, hanem az attribútum
paraméterében megadott nevet fogja figyelembe venni az MVC. A következő példához tartozó URL
path: ActionDemo/Szerkesztes/5 lehet.

[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.

Kategória Interface Feladatkör


Hitelesítés IAuthenticationFilter A requestet küldő hitelesítése
Engedély IAuthorizationFilter Az action egyáltalán végrehajtható az aktuális
request környezete, jogosultság szerint, vagy más a
teendő?
Action végrehajtás IActionFilter + Az action futása előtt és után végrehajtott közös
IResultFilter kódok.
Hibakezelés IExceptionFilter Az action futása során fellépő hibák egységes
kezelése.

A lista sorrendje a filterkategóriák feldolgozásának a sorrendjét is jelenti. Nézzük most ezeket.


5.9 A kontroller és környezete - Filterek 1-111

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.

Ez az interfész előír egy OnAuthenticationChallenge metódust is. Ezzel a metódussal bármelyik


később induló filter Result propertyjébe töltött eredményt felülbírálhatjuk azzal hogy a
OnAuthenticationChallenge(context) context.Result tulajdonságát feltöltjük. Ez amolyan végső
döntési lehetőséget ad.

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.

Engedély és érvényesség vizsgálata (IAuthorizationFilter)

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

Az MVC ennek az absztrakt típusnak csak egy megvalósítását tartalmazza az AuthorizeAttribute


formájában. Ezzel az attribútummal ellátott Actiont csak bejelentkezett állapotban és megfelelő
jogosultságok birtokában lehet elérni. Ez visszapörgetve a route logikán azt jelenti, hogy egy URL
5.9 A kontroller és környezete - Filterek 1-112

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 példaalkalmazást használva a http://localhost:18005/ActionDemo/Edit/1 oldalon a vásárló címébe


beírom, hogy "<script>alert('Viszem a cookidat!');</script>" . Ami egy javascript injekció
akar lenni.

A böngészőben postolom a formot, akkor szerencsére hibaüzenetet kapok:

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.

Megjelenés Html markup részlet


<div class="display-field">
Budapest 2 &lt;script&gt;alert(&#39;Viszem a
cookidat!&#39;);&lt;/script&gt;
</div>

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>

Jöhetnek a hackerek, mert indul a javascript:

Nagyon-nagyon meggondoltan használjuk ezt a lehetőséget, mert az alkalmazásunkat és a


felhasználóinak a személyes adatait is veszélyeztetjük ezzel! Amikor tényleg arra van szükségünk, hogy
a felhasználótól származó HTML tartalmat jelenítsünk meg, akkor alkalmazzunk jól bevált HTML
szűrőket. Ezek paraméterezhetőek olyan szempontból, hogy melyik HTML taget engedünk be és miket
nem a felhasználótól. Hogy miről is van szó, azt jól bemutatja a http://htmlpurifier.org/demo.php site.
Az ASP.NET-hez illeszkedő, szabályozott HTML szűrést is biztosító Microsoft Web Protection Library
letölthető a http://wpl.codeplex.com/ oldalról.
5.9 A kontroller és környezete - Filterek 1-114

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")

Ilyenkor az action kimeneti eredménye beékelődik a Html.Action hívás helyére. Ezzel az a


ChlidActionOnly attribútummal letilthatjuk, hogy más módon elérhető legyen az az action. A
böngészőből, URL alapján nem lehet majd elérni, ha ez az attribútum szerepel az action metóduson.

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.

Action végrehajtás (IActionFilter) és filter konfigurálás

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

Metódusnév Feladat Naplóba kerül


EnterMethod(string method) naplózza, hogy beléptünk egy „18:29:45.003 Enter
action metódusba ’BusinessCritical’ action”
Store(string message) a message tartalmát bejegyzi a „Ez egy szöveg, amit
naplóba naplóztunk”
ExitMethod(string method) bejegyzi, hogy kiléptünk az „18:29:45.011 Exit
action metódusból ’BusinessCritical’ action”

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!):

public ActionResult BusinessCritical()


{
Services.SillyLogger.EnterMethod("BusinessCritical");
//..
//Itt sok kód lesz
//..
Services.SillyLogger.Store("Ez egy szöveg, amit naplóztunk");
//..
//Itt sok kód lesz
//..
ViewResult result = View();
Services.SillyLogger.Store("A result: " + result.View);
Services.SillyLogger.ExitMethod("BusinessCritical");
return result;
}

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:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]


public abstract class ActionFilterAttribute : FilterAttribute, IActionFilter, IResultFilter
{
public virtual void OnActionExecuting(ActionExecutingContext filterContext) {} //1.

//Action meghívása

public virtual void OnActionExecuted(ActionExecutedContext filterContext ) {} //2.

public virtual void OnResultExecuting(ResultExecutingContext filterContext) {} //3.

public virtual void OnResultExecuted(ResultExecutedContext filterContext) {} //4.


}

Érdemes megfigyelni az AttributeUsage attribútumot, miszerint az ActionFilter és leszármazottai


osztályra és metódusra is illeszthetőek. Ennek később még lesz szerepe.

Az OnAction… metódusok az action metódusok végrehajtására, az OnResult… a renderelésre


vonatkoznak. A …Executing metódusokat előtte a …Executed metódusokat utána hívja az MVC. A hívási
sorrendet inline megjegyzésként beleírtam az előbbi definícióban a sorok végére (1.2.3.4.).
5.9 A kontroller és környezete - Filterek 1-116

Íme, a naplózást végző ActionFilter megvalósítás:

public class SillyLoggerActionFilterAttribute : System.Web.Mvc.ActionFilterAttribute


{
public override void OnActionExecuting(System.Web.Mvc.ActionExecutingContext filterContext)
{
SillyLogger.EnterMethod(filterContext.ActionDescriptor.ActionName);
}

public override void OnActionExecuted(System.Web.Mvc.ActionExecutedContext filterContext)


{
SillyLogger.ExitMethod(filterContext.RouteData.Values["action"].ToString());
}

public override void OnResultExecuted(System.Web.Mvc.ResultExecutedContext filterContext)


{
ViewResult result = filterContext.Result as ViewResult;
if (result != null)
SillyLogger.Store("Használt view neve: " + result.ViewName);

RazorView razor = result.View as RazorView;


if (razor != null)
SillyLogger.Store("Használt view template: " + razor.ViewPath);

//var response = filterContext.HttpContext.Response;


//response.Filter = new LogFilter(response.Filter,
filterContext.RouteData.Values["action"].ToString());

SillyLogger.Store(string.Format("{0} action and view processed",


filterContext.RouteData.Values["action"]));
}
}
12. példakód

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 OnActionExecuted azután következik, hogy az actionből kiléptünk, ezt is naplózhatjuk. Ha


összehasonlítjuk a két metódust látható, hogy legalább két módon is elérhető a szóban forgó action
metódus neve (tehát nem kell reflexió a metódus név megtalálásához és a naplózáshoz).

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

A működés kipróbálásához egy letisztult actiont tudunk használni.

[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.

Mint utaltam rá az ActionFilter és valójában az összes FilterAttribute-t megvalósító leszármazottal


kidekorálható a Controller ősosztály is, nem csak az egyes actionök. Ennek az lesz a következménye,
hogy a filterünk a kontroller minden actionje esetén működésbe fog lépni, úgy mintha minden egyes
actionre ráillesztettük volna. Ez egy loggolási attribútumnál nagyon jó, hisz a cél az volt, hogy minden
action használatát naplózzuk. Az MVC tovább kényeztet minket, mert azt is megtehetjük, hogy az
alkalmazásunk összes actionje esetén működésbe lépjen az attribútumunk, anélkül, hogy bármelyik
actionre vagy kontrollerre rátennénk.

Ha ellátogatunk a projektünk App_Start/FilterConfig.cs fájljába, akkor itt


megnézhetjük, hogyan kell ezt csinálni. A Visual Studio által generált
projektben a HandleErrorAttribute hozzá van adva filters gyűjteményhez.
Ha ebbe a gyűjteménybe illesztünk egy filter példányt, akkor azt minden
actionnel kapcsolatban használni fogja az MVC. Ha beletesszük a SillyLoggerActionFilterAttribute –ot,
akkor az alkalmazásunk összes action futása naplózva lesz.

public static void RegisterGlobalFilters(GlobalFilterCollection filters)


{
filters.Add(new HandleErrorAttribute());
filters.Add(new Services.SillyLoggerActionFilterAttribute());
}

Három helyen (Scope-ban) ’élesíthetjük’ a filtereinket.

 Globálisan – A GlobalFilterCollection –ban. (fenti példa)


 Kontrolleren – Osztály attribútumként
 Actionön – Metódus attribútumként.

A lista egyben a kiértékelés sorrendjét is jelenti. Lehetőségünk van a sorrend megváltoztatására az


összes FilterAttribute leszármazott számára az Order propertyn keresztül -1 (default) vagy annál
nagyobb számmal. Minél nagyobb annál előbb kerül feldolgozásra. Ha azonos Order értéket adunk,
akkor a Scope normál sorrendje dönt.

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

ugyanazok a virtuális metódusai, mint az attribútumnak. Ami a legfontosabb, hogy a végrehajtási


sorrendben a Controller ezen metódusai az elsők és utána jönnek a Filter attribútumok metódusainak
a hívásai. Ezért, ha valamilyen oknál fogva mindenképpen elsőként szeretnénk részt venni az action
végrehajtással kapcsolatos eseménysorban, akkor az ilyen kódot a kontrollerben lehet megvalósítani a
már megismert OnActionExecuting-al és három társával. Ha a saját kontrollereinket nem közvetlenül
a Controller ősből származtatjuk, hanem beékelünk a származási láncba a kettő közé egy saját
(controller)base class-t, akkor lehetőségünk van abban implementálni az IActionFilter interfészt. Az
összes saját kontrollerünket ebből a base class kontrollerből származtatva szintén elérhetjük, hogy
minden kontroller minden actionje esetén lefussanak az IActionFilter előírt metódusai. Mondtam, hogy
az MVC-ben szinte minden feladatra legálabb két megoldás is adható.

OutputCacheAttribute

Ezzel az attribútummal jelezhetjük az MVC frameworknek, hogy az action+View futásának


végeredményeként létrejött HTML tartalmat ideiglenesen tárolja el. Amikor az actiont legközelebb újra
kéne futtatni egy új request miatt, az action futtatása helyett az előzőleg létrejött HTML tartalmat fogja
visszaküldeni a böngészőnek. Megadhatunk lejárati időt, ami után érkező request esetében az actiont
fogja újrafuttatni az előzőleg cache-elt HTML eredmény helyett. Szintén megadhatunk számos
feltételt, hogy milyen request paraméterek szerint válassza külön a tárolt eredményeket (URL
paraméter, HTTP header tartalom, stb.). Ezzel az attribútummal a 10.1-es fejezetben igen részletesen
meg fogunk ismerkedni.

Hibakezelés (IExceptionFilter)

HandleErrorAttribute

Ha egy actionben hiba történik, akkor az MVC alapértelmezett nagy sárga hibaüzenő oldala jelenik
meg.

public ActionResult MakeException()


{
throw new InvalidOperationException("Opps. Hiba történt.");
}

Ezt a helyzetet többféleképpen is tudjuk kezelni. Az egyik, ha a HandleError attribútumot ráillesztjük


az actionre. Azonban, hogy ez az egyedi hibakezelés elinduljon, a web.config-ban ezt engedélyezni kell
a system.web ágban:

<system.web>
<customErrors mode="On"/>

Próbáljuk meg elérni a /ActionDemo/MakeException oldalt anélkül, hogy használatba vennénk az


attribútumot. Ennek máris van eredménye:

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";
}

A HandleErrorInfo tartalmazza a hiba körülményeit. Ezt felhasználhatjuk a hibamegjelenítő View-ban.

A HandleErrorAttribute rendelkezik néhány hasznos paraméterrel:

 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.

Hozzuk létre az alábbi hibamegjelenítő View-t:

@model HandleErrorInfo
@{
ViewBag.Title = "HandleException";
}

<h2>Egyedi, Action függő hibamegjelenítés</h2>

Action neve: @Model.ActionName


<br />
Controller neve: @Model.ControllerName
<br />
Hibaüzenet: @Model.Exception.GetBaseException().Message
<br />
<br />
Bocsi.

Készítsünk egy MakeException actiont és egy további bug generátort MakeGeneralException néven:

[HandleError(View = "HandleException", ExceptionType = typeof(InvalidOperationException))]


public ActionResult MakeException()
{
throw new InvalidOperationException("Opps. Hiba történt.");
}

public ActionResult MakeGeneralException()


{
throw new Exception("Opps. Generális hiba történt.");
}

A MakeException attribútuma úgy van meghatározva, hogy a HandleException View-t használja


hibamegjelenítőnek, de csak InvalidOperationException típust kezelje le. Ha megpróbáljuk elérni az
oldalt ezt az eredményt kapjuk:
5.9 A kontroller és környezete - Filterek 1-120

A másik action a globális HandleError-t használja a FilterConfig-ból és az eredménye is ennek


megfelelően a piros angol hibaüzenet.

Persze megtehetjük, hogy az egyedileg paraméterezett HandleError-t szintén áttesszük a FilterConfig-


ba és akkor lesz egy globális InvalidOperationException-t kezelő egyedi hibamegjelenítőnk. Mellesleg
ki is próbálhatjuk az Order tulajdonság hatását is, mert ahhoz, hogy a speciálisabb
InvalidOpertaionException (Exception leszármazott) előbb lekezelje a hibát, minthogy az eljusson az
általánosabb HandleError-hoz, a sorrendet is be kell állítani.

public static void RegisterGlobalFilters(GlobalFilterCollection filters)


{
filters.Add(new HandleErrorAttribute()
{
Order = 10,
View = "HandleException",
ExceptionType = typeof(InvalidOperationException)
});
filters.Add(new HandleErrorAttribute() {Order = 1});
}

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.

Hogy ez a globális hibakezelés rendesen működjön minden kontrollerrel, a HandleException.cshtml


fájlt át kell helyezni a Views/Shared mappába. Mert ez az kontrollerek által közösen használható View-
k gyűjteménye.

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

Tehát következzenek a template-ek. Ebben a keretrendszerben nem lehetséges, hogy a kontroller a


View-n implementált kódokat hívjon meg, ahogy a kontroller példányunk sem érhető el a View
kódjából. Szerencsére elég jól le vannak választva egymásról. A kommunikációs lehetőség csak tároló
objektumokon keresztül oldható meg a View és a kontroller között. Ennek elsődleges használati
formája a modell és az ActionResult. A továbbiak lehetnek még a ViewData, ViewBag, TempData,
Session és egy pár egyéb nem ajánlott lehetőség.

6.1. A View mappák

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.

Az alapértelmezett View fájlnév és a kontroller osztálynév összerendelési konvenció nagyon egyszerű,


valahogy így néz ki:

AkarmiController.Index() -> Views/Akarmi/Index.cshtml.

public class ActionDemoController : Controller


{
public ActionResult Index()
{
return View();
}
}

Í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

6.2. A View fájl kiválasztása

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.

public class ViewTestController : Controller


{
//Ennek az actionnek nincs View párja.
public ActionResult NonexistentPage()
{
return View();
}
}

Indítom a hozzá való URL-el (/ViewTest/NonexistentPage). Az eredményt érdemes megfigyelni


alaposan. Az itt látható hibaüzenet lehetne a nagy sárga kép is, most csak azért nem az jelenik meg,
mert nem sokkal ezelőtt a filtereknél (5.9 - Hibakezelés) átállítottuk a hibaüzenet megjelenítését, de a
lényeg ott van a végén.

Action neve: NonexistentPage


Controller neve: ViewTest
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.aspx
~/Views/ViewTest/NonexistentPage.ascx
~/Views/Shared/ NonexistentPage.aspx
~/Views/Shared/ NonexistentPage.ascx
~/Views/ ViewTest/NonexistentPage.cshtml
~/Views/ViewTest/NonexistentPage.vbhtml
~/Views/Shared/ NonexistentPage.cshtml
~/Views/Shared/NonexistentPage.vbhtml

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:

Fájl kiterjesztések: .aspx, .ascx, .cshtml, .vbhtml Útvonalak: Views/ViewTest/ és Views/Shared

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:

private void Application_Start()


{
ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new RazorViewEngine());

Ezzel kitöröltük a Web Forms View motort (.ascx, .aspx) és csak a razor értelmezőt (.cshtml,.vbhtml)
tettük vissza.

„Hibából tanul az ember” alapon nézzük ezután a Vakoldal hibaüzenetét:

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:

public class ConciseViewEngine : RazorViewEngine


{
public ConciseViewEngine()
{
this.AreaViewLocationFormats = new[] { "~/Areas/{2}/Views/{1}/{0}.cshtml",
"~/Areas/{2}/Views/Shared/{0}.cshtml"};
this.AreaMasterLocationFormats = new[] {"~/Areas/{2}/Views/{1}/{0}.cshtml",
"~/Areas/{2}/Views/Shared/{0}.cshtml"};
this.AreaPartialViewLocationFormats = new[] {"~/Areas/{2}/Views/{1}/{0}.cshtml",
"~/Areas/{2}/Views/Shared/{0}.cshtml"};
this.ViewLocationFormats = new[] {"~/Views/{1}/{0}.cshtml",
"~/Views/Shared/{0}.cshtml"};
this.MasterLocationFormats = new[] {"~/Views/{1}/{0}.cshtml",
"~/Views/Shared/{0}.cshtml"};
this.PartialViewLocationFormats = new[] {"~/Views/{1}/{0}.cshtml",
"~/Views/Shared/{0}.cshtml"};
this.FileExtensions = new[] { "cshtml" };
}
}
13. példakód

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

A global.asax-ban használjuk ezt az új osztályt ViewEngine-ként:

private void Application_Start()


{
ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new ConciseViewEngine());

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.

Egyszerűen hozzunk létre bármelyik View fájlból egy olyan másolatot,


aminek a fájlkiterjesztése elé beírjuk azt, hogy '.Mobile'. Ezek után, ha a
hozzátartozó oldalt megnyitjuk egy mobil eszköz böngészőjéből, akkor
az Index.Mobile.cshtml tartalma fog érvényesülni. Míg hagyományos
asztali gép böngészője számára az eredeti Index.cshtml lesz a mérvadó. A tabletek desktop eszköznek
számítanak. Ez a fájlnév konvenció működik Partial View és Layout esetében is.

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

6.3. Tartalma, típusos View

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.

Az előugró ablakon válasszuk ki a modellt és a többit is állítsuk be ilyenformán:

A modell mellett állítsuk be, hogy a Scaffold


template-ek közül a Details sablont használja a
View fájl elkészítéséhez. Az eredmény a
szokásos típusos View lesz, mivel amit a modell
alapján generál, abban a modell propertyjeit
típusosan éri el.

@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

Tudjuk, hogy a PurchasesList lista elemeinek


típusa ActionDemoPurchaseModel. Ez alapján a
„List” scaffold template segítségével egy
táblázatot tudunk generáltatni számára, ami lehet
egyből partial View is. Még két lépés kell, hogy
kész legyen a teljes Detail View.

A kontrollerben át kell adni a View típusának megfelelő modellt:

public ActionResult Details(int id)


{
return View(ActionDemoModel.GetModell(id));
}

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:

<div style="border:1px solid;padding: 5px">


<h4>@Html.DisplayNameFor(model => model.PurchasesList)</h4>
@Html.Partial("DetailList",Model.PurchasesList)
</div>

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>

Nálam a böngészőben ez az eredmény volt látható. Még


mindig lenne vele munkánk, mert a Vásárlások listája
sorainak a végén az Edit, Details, Delete mögött nincs action
metódus (mivel nem írtuk meg). Ezen kívül új sort sem
tudunk hozzáadni a „Create New” linkkel. Most azonban
haladjunk tovább, mert ez a tapasztalat tovább is visz minket
a View-k egymásba ágyazásának kifejtéséhez.
6.4 A View - Partial View 1-127

6.4. Partial View

Lehetséges és érdemes a View-t darabokra bontani aszerint, hogy az adott darab előállítása

más modellt igényel (a főmodellben egy propertybe van ágyazva a modellje).


Ez szerepelt az előző fejezet
példájában.

a View-ban többször is szerepel azonos oldalon. (egy lista egy-egy soraként)


Ez nagyon jól jöhet, ha a
listaelemek megjelenítése
bonyolult. Például egy
könyvkatalógus elemeinél,
ahol a könyv borítója, címe,
írója, értékelése, szokott az
általános megjelenés lenni.

más View is használja a tartalmát. (újrahasznosítás.)


Ez lehet szintén egy bonyolult
modell kijelzője, aminél azt
szeretnénk elérni, hogy a
megjelenítése egységes
legyen minden oldalon. Ide is
jó a könyvkatalógus példa és
az előzővel is kombinálható.

tartalmát később önmagában frissíteni szeretnénk. (AJAX módon)


Maradva a könyvkatalógusnál
és annak is az értékelés
propertyjénél, aminek a
megjelenése az ismerős a
vezérlő is lehetne.
Ahhoz, hogy beállítsam a
véleményem osztályzatát, nem érdemes az egész oldalt újratölteni. Beállítom és ennek hatására
az eredmény továbbítódik a szerverre, ahol egy action feldolgozza és tárolja az értékelésemet.
6.4 A View - Partial View 1-128

egy teljesen más action feladata (child action)


Ebben az esetben arról lehet szó,
hogy a teljes View előállítása több
egymástól teljesen független
modellből származik, esetleg más
kontroller felelős a kezeléséért.
Ennek a partial darabkái saját
életet élnek. Ez lehet például a főmenü előállítása, vagy a (jobb felső sarokhoz szokott) profile
adatok kezelése.

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

Minél sötétebb a háttérszín, annál mélyebben vagyunk a View-k egymásba ágyazásának a


hierarchiájában. A játékban három darab ViewData beállítás is szerepel. Az 1. a „J. Gipsz”, a 2. a „St. II.
Gipsz”, a 3. a Mr. III. Gipsz. Ezeket, a három egymásba ágyazási szint használja. Ami látható, hogy a
normál partial és a child partial View-kban is el lehet érni a szülő ViewData objektumát a sajátján kívül
(ami nyilvánvaló). this.ViewContext.ParentActionViewContext.ViewData;

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.

Emlékeztetőnek szántam, hogy a ViewData és a ViewBag azonos tárolással rendelkezik a háttérben


(Main ViewBag->ViewData)

public class PartialDemoController : Controller


{
public ActionResult Index()
{
//ViewData tartalma:
ViewData["Szemelynev"] = "J. Gipsz";
ViewData["Címe"] = "9999 Salátahegye 1.";

//ViewBag tartalma
ViewBag.Telefonszama = "+99 99-999-999";
ViewBag.EmailCime = "kukac@kukac.kc";

TempData["Tempadat"] = "Elérhető!";
return View();
}

public ActionResult IndexRedir()


{
TempData["Tempadat"] = "IndexRedir Elérhető!";
return RedirectToAction("DemoTempData");
}

public ActionResult DemoTempData()


{
6.4 A View - Partial View 1-130

return View();
}

public ActionResult DemoPartial()


{
return PartialView();
}

//Első szintű action


[ChildActionOnly]
public ActionResult ChildAction()
{
ViewData["Szemelynev"] = "St. II. Gipsz";
ViewData["Címe"] = "1111 Paradicsomvölgy 2.";
return PartialView();
}

//Második szintű action


[ChildActionOnly]
public ActionResult SecondLevelChildAction()
{
ViewData["Szemelynev"] = "Mr. III. Gipsz";
ViewData["Címe"] = "2222 Káposztafelföld 2.";
return PartialView();
}
}

Az Index.cshtml:

@{
ViewBag.Title = "Demo Index";
}
<style type="text/css">
.left { float: left;width: 30%;}
</style>

<h2>Partial demo kontroller Index</h2>


<div style="background-color: #ddd">
<div class="left"> <h4 style="">Main ViewData</h4>
Szemelynev: @ViewData["Szemelynev"]<br />
Címe: @ViewData["Címe"]
</div>
<div class="left"> <h4>Main ViewBag</h4>
Telefonszama: @ViewBag.Telefonszama<br />
EmailCime: @ViewBag.EmailCime
</div>
<div class="left"> <h4>Main ViewBag -> ViewData</h4>
Szemelynev: @ViewBag.Szemelynev<br />
Címe: @ViewBag.Címe
</div>
<div class="clear-fix"></div>
<b>Tempdata: @TempData["Tempadat"]</b> @Html.ActionLink("Tempdata demó", "IndexRedir")
</div>
<hr />
<div style="background-color: #ddd; margin-left: 20px;">
<h3>*** PartialView eleje ***</h3>
@Html.Partial("DemoPartial")
<h3>*** PartialView vége ***</h3>
</div>
<div class="clear-fix"></div>
<hr />
<div style="background-color: #ddd; margin-left: 20px;">
<h3>*** Child Action eleje ***</h3>
@Html.Action("ChildAction")
<h3>*** Child Action vége ***</h3>
</div>

Kis bogarászás után megtalálhatjuk a partial View-t (@Html.Partial("DemoPartial")) beágyazó és a child


actiont meghívó (@Html.Action("ChildAction")) Html helper metódusokat.
6.4 A View - Partial View 1-131

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>

A ChildAction.cshtml Partial View kódja (a SecondLevelChildAction.cshtml-t nem annyira fontos


megnézni):

@{
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 fejlesztés során viszont a child action önmagában is tesztelhető, kipróbálható.

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.

TempData View kódja:

TempData tartalma: <b>@TempData["Tempadat"]</b>


<br />
@Html.ActionLink("Tempdata demó újra", "DemoTempData")

Ha a Tempdata demó linkre kattintunk első esetben az eredmény:


6.4 A View - Partial View 1-132

Ha most a Tempdata demó újra linkre kattintunk, akkor a TempData tartalma már nem lesz elérhető,
csak az üres hely látszik:

Hogy ez ne történjen meg használhatjuk a TempData.Keep() metódust.

public ActionResult DemoTempData()


{
TempData.Keep("Tempadat");
return View();
}

A Keep() (paraméter nélkül) a TempData összes bejegyzését megtartatja. A paraméterrel pedig


pontosan meg tudjuk mondani, hogy melyiket tartsa meg. Ezzel biztosítható, hogy a következő request
folyamán még mindig elérhető legyen a bejegyzésünk. A RedirectResult és a RedirectToRouteResult
használata azt eredményezi, hogy a háttérben meghívódik a Keep() metódus paraméter nélküli
változata.

Figyelem! A TempData viselkedése megváltozott az MVC4-ben az előző verziókhoz képest. Az MVC 2


és 3-as verziójában a TempData bejegyzés, túlélt egy request - response ciklust, ha nem hivatkoztunk
a bejegyzésre (nem is olvastuk ki a tartalmát). Az oldal kiszolgálása utáni következő request alatt
létrejött kontrollerben még elérhető volt az előző ciklus TempData tartalma. Az előző példában azzal,
hogy TempDate[„Tempadat”] –ból kivettük az adatot az MVC3-ban a következő menetben már nem
lesz elérhető a Tempadat által indexelt tartalom, ha nem vettük volna ki ott maradt volna. Az MVC4-
ben nem számít, hogy kivettük-e vagy nem, mindenképpen megszűnik a tartalom. (Kivéve a Keep()
metódus használata, ahogy láttuk). Ezt MVC verzióváltás alkalmával figyelembe kell venni.

Partial View fájl kiválasztása

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")

Ez a módszer használható a View és partial View fájlok meghatározására az actionökben is:

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ő.

6.5. A View-k egymásba ágyazása

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.

Nézzük az egymásba ágyazás szereplőit:

_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";
}

Ez azt közli az MVC-vel, hogy a View-k összeállításához használja mester oldalnak a


~/Views/Shared/_Layout.cshtml tartalmát. Ebből a fájlból – mint mondtam – csak egy szokott lenni,
de ez sem kötelező egy ilyen testre szabható keretrendszerben. Lehet minden View mappában egy.
Ezzel közvetetten, kontrollerenként (kontroller név -> View mappa név) tudunk biztosítani egy közös
szabályt a View-k számára. Ha mondjuk, megmaradunk annál, hogy ez csak a mesteroldal fájlját
határozza meg, akkor kontrollerenként lehet meghatározni egy-egy mesteroldalt. Ráadásul ez a
_ViewStart hasonlóan értelmezett, mint a web.config, azaz ha meghatározunk egyet a Views
mappában (és így érvényes lesz minden kontroller minden View-jára is), akkor ezt még az almappákban
felülbírálhatjuk. Persze nem kell megmaradnunk annál, hogy csak a _Layout fájlt határozzuk meg,
bármi mást is beletehetünk, amit annyira fontosnak érzünk, hogy minden View-ban megjelenjen.

@{ 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.

Létrehoztam egy lecsupaszított layout fájlt _LayoutDemo.cshtml néven ezzel a tartalommal:

<!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):

Egyben Page tartalma: @Page.SzovegesTartalom<br />

A @RenderPage fenti használatának egy alternatívája ez a kicsit zavarosabb kóddarabka:

@{Html.RenderPartial("~/Views/ViewTest/EgybenPage.cshtml", new { SzovegesTartalom = "Render page


demó" }); }

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 Layout-ból lehetőségünk van megtudni, hogy az őt felhasználó aktuális View megvalósította-e az


adott section-t. Erre szolgál az IsSectionDefined(”<sectionName>”) metódus.

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

Ennek kipróbálására a View-ban a Layoutot átállítottam egy köztes _LayoutDemoSub.cshtml –re:

@{
Layout = "~/Views/Shared/_LayoutDemoSub.cshtml";
}

A köztes Layout fájl tartalma:

@{
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>

Ennek a Layout-ja az eredeti template fájl. Most a hierarchia:

 _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)
}

Ez a továbbítás nem opcionális. Amit a fő Layout-ban meghatároztam renderelendő sectionnak, azt


tartalmazni kell a szub Layout-oknak is. Itt viszont nem kötelező, hogy a továbbításért felelős
@RenderSection-t tartalmazzon. Remélem érthető, mindenesetre itt az eredménye:
6.5 A View - A View-k egymásba ágyazása 1-137

A vékony kék keret a <body> tag-et jelenti. A


zöld pontvonal a szub Layout-ban meglévő
továbbítást és a szub Layout-ban megadott
tartalmat jelenti.

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>
}

Ez pedig az Egyben.cshtml View-ben:

@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.

public ActionResult Egyben()


{
return View("Egyben","_LayoutDemoSub");
}

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:

protected internal ViewResult View(string viewName, string masterName)

Az action visszatérési ViewResult-ban található property neve szintén: MasterName.

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

6.6. A View nyelvezete

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.

6.6.1. Razor szintaxis

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 <%}%>)

<%if (1==1) { %>


Olyan Html szöveg, ami megjelenik,
ha a fenti feltétel igaz.
<%}%>

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.

 Magic karaktereket használnak a template nyelv kulcsszavaihoz


 A modellváltozók értékeinek egyszerű kiírtatását biztosítják
 A szokásos programozási szerkezetekre egyszerű szintaxist biztosítanak. (kódblokk, if-es
szerkezet, iterációk)

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.

Egy változó tartalmának kiíratása, magában az Oldalcím1 statikus szöveg után:

Oldalcím1: @this.ViewBag.Title <br />

Változó beágyazva <b> elemek közé. A Title szót azonnal követheti a </b> nem kell space:

Oldalcím2: <b>@this.ViewBag.Title</b> <br />

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.

Oldalcím3: @this.ViewBag.Title; <br />

Az „Oldalcím4” után kiíratásra került a böngésző neve.


6.6 A View - A View nyelvezete 1-140

Oldalcím4: @Request.Browser.Browser<span>.Nem létező property</span><br />

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…)

Link: <a href="@Request.Url">Vissza ide.</a><br />

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.

Egysoros, több utasításos szakasz:<br />


@("Az aktuális URL: " + Request.Url)<br />
@(Request.Browser.Browser).Nem létező property<br
/>

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.

Email cím: baratom@mvc.hu<br />

Egy db kukac: @@<br />

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.

if (valami == null) continue;

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:

@if(valami == null) { continue; }

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:

<li data-showitem="@(Model.Decision ? "hide":"show")" >

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.

<div class="foosztaly @ViewBag.CssOsztaly" id="div1">


Div1
</div>

<div class="@ViewBag.CssOsztaly" mindegynevu="@ViewBag.CssOsztaly" id="Div2">


Div2
</div>

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.

<div data-mindegy="@ViewBag.CssOsztaly" id="Div3">


Div3
</div>

A keletkező HTML kódban ott marad az értékadás nélküli data-mindegy attribútum.

Következzen két checkbox definíció, aminek a HTML szabvány szerint a checked=”checked”


attribútum-érték pár jelenti a bejelölt állapotot. (a böngészőknek mindegy mi az érték és az üres
attribútumot is jelöltnek értelmezik, de nem ez az eredeti játékszabály). Az isChecked null vagy false
értékénél a checked attribútumot törli, true esetén checked=”checked” lesz. (bármi más esetén azt az
értéket adja az attribútum értékeként). A readonly és disabled HTML attribútumokkal ugyan ez a
helyzet.

<input type="checkbox" checked="@ViewBag.isChecked" readonly="@ViewBag.isChecked"


disabled="@ViewBag.isChecked" />

@{ViewBag.isChecked = true;}

<input type="checkbox" checked="@ViewBag.isChecked" readonly="@ViewBag.isChecked"


disabled="@ViewBag.isChecked" />

Egy kis ínyencség következik. Vajon mi lesz ennek az input definíciónak a renderelt HTML eredménye?

<input type="textbox" value="@ViewBag.isChecked"/>

Ha az isChecked egy boolean true érték: <input type="textbox" value="value" />


Ha az isChecked egy boolean false érték: <input type="textbox" />

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.

A megjegyzést így kell megadni:

@*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 class="foosztaly" id="div1">


Div1
</div>

<div id="Div2">
Div2
</div>

<div data-mindegy="" id="Div3">


Div3
</div>

<input type="checkbox" />


<input type="checkbox" checked="checked" readonly="readonly" disabled="disabled" />

<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.

6.6.2. Kód a View-ban

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();
}

Nálam a viewTipusa ez volt: ASP._Page_Views_nezet_ViewContext_cshtml


6.6 A View - A View nyelvezete 1-144

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:

public ActionResult ViewContext()


{
return View();
}

A View a saját típusinformációit írja ki:

@{ Type viewTipusa = this.GetType(); }

<h2>ViewContext felfedése</h2>
A View típusa: @viewTipusa <br />
A View dll fájlja: @viewTipusa.Assembly.Location

A futás eredménye nálam ez volt:

A View típusa: ASP._Page_Views_ViewTest_ViewContext_cshtml

A View dll fájlja: C:\Windows\Microsoft.NET\Framework\v4.0.30319\Temporary ASP.NET


Files\root\3db22504\59dcd6a3\App_Web_0ejbcrcy.dll

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.

public class _Page_Views_ViewTest_ViewContext_cshtml : System.Web.Mvc.WebViewPage<dynamic>


{
protected ASP.global_asax ApplicationInstance
{
get { return ((ASP.global_asax)(Context.ApplicationInstance)); }
}

public override void Execute()


{
Type viewTipusa = this.GetType(); //Ezt írtam a View elejére.
WriteLiteral("\r\n\r\n<h2>ViewContext felfedése</h2>\r\nA View típusa: ");
Write(viewTipusa); //@viewTipusa
WriteLiteral(" <br />\r\nA View dll fájlja: ");
Write(viewTipusa.Assembly.Location); //@viewTipusa.Assembly.Location
WriteLiteral("\r\n");
}
}
6.6 A View - A View nyelvezete 1-145

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):

public override void Execute()


{
WriteLiteral("<!DOCTYPE html>\r\n<html");
WriteLiteral(" lang=\"en\"");
WriteLiteral(">\r\n <head>\r\n <meta");
//...
WriteLiteral("\r\n </head>\r\n <body>");
//...
Write(RenderSection("featured", required: false));
//...
Write(RenderBody());
//...
WriteLiteral("\r\n </body>\r\n</html>\r\n");
}
Láthatóak a „featured” section és a RenderBody generálásának az eredményeit fogja kiírni a
háttérben dolgozó TextWrite-re, ami a response-t tölti majd fel.

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:

Mély beágyazás Nincs beágyazás


if (objektum != null) if (objektum == null || objektum.prop == null) return;
{
if (objektum.prop != null) //Lényegi kód
{
//Lényegi 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.

Ez a dinamikus fordítási metodika megengedi, hogy a View szöveges tartalmát futásidőben


szerkesszük. Ezért nem kell újraindítani az alkalmazást, ha a View-ban változtattunk. A változást
észleli az MVC és újrarendereli a View-t -> újra előállítja belőle a C# kódot és a dll-t.

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

 Az első a drasztikus módszer: A projekt fájlt ( <ProjektNév>.csproj ) külső


szövegszerkesztővel megnyitjuk és belejavítunk, majd elmentjük. Ezt észreveszi a VS és egy
ablakban figyelmeztetni fog, hogy a projektfájlt valaki módosította és felteszi a kérdést, hogy
azt újra betöltse-e. (Igen)

 A steril módszer, hogy a projekt nevén kérünk egy helyi menüt.

Majd az „Unload Project” menüponttal a VS elengedi a projektünket.

Utána újra helyi menü. Ezúttal az „Edit <ProjektNév>.csproj” –t válasszuk.


Ezzel megnyitottuk a projekt fájl egy XML szerkesztőben. (A szerkesztés és
mentés után a Reload Project menüponttal vissza tudjuk tölteni a
projektet.)

Mindkét módszer esetén, a XML formátumú projektfájl elején keressük meg a


<MvcBuildViews>false</MvcBuildViews> bejegyzést és írjuk át a „false” –t „true”-ra. Fájl mentés és a
projekt betöltése után a következő fordításkor a View-k is fordításra kerülnek. Próbáljunk szintaktikus
hibát írni valamelyik View C# kódszakaszba, hogy lássunk eredményt, akarom mondani fordítási
hibát.

6.6.3. Razor kulcsszavak

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.

@Html.ActionLink("Ugras a szintaxis oldalra", "Szintaxis", null, new {id="ug1"} )

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.

@Html.ActionLink("Ugrás a szintaxis oldalra","Szintaxis", null, new {id="ug1", @class = "kiemelt"} )

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ó:

@Html.ActionLink("A szintaxis oldalra 2", "Szintaxis", null, new RouteValueDictionary() { { "id",


"ugras1" }, { "class", "kiemelt" }, { "data-html5", "Minden egyszerű" } }

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

@helper ListItemTemplate(int index)


{
<li>Elem sorszáma: <b>@index</b></li>
}

<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 példakód a /Views/Razor/Inside.cshtml-ben van.

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.

Leginkább egy iteráción belül érdemes használni. A baloldalon a fenti kódblokk


eredménye. Hasznos lehet, ha csak a View-n belül szeretnénk használni egy
template darabot és meg akarunk spórolni egy Partial View írást.

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");
}

A demó eredménye: "@ic.MessageTxt"<br />


Szám éréke: @numberTen<br />
Tanács: @txt<br />
<div class="@GetCssClass()"></div>

@functions
{
int numberTen = 10;
string txt = "Kerüld el, ha teheted!";

private class InsideClass


{
public InsideClass(string s)
{
this.MessageTxt = s;
}
public string MessageTxt { get; private set; }
}

private HtmlString GetCssClass()


{
return new HtmlString("cssoszalynev");
}
}

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

public class _Page_Views_Razor_Inside_cshtml : System.Web.Mvc.WebViewPage<dynamic>


{
public System.Web.WebPages.HelperResult ListItemTemplate(int index)
{
return new System.Web.WebPages.HelperResult(__razor_helper_writer =>
{
WriteLiteralTo(__razor_helper_writer, " <li>Elem sorszáma: <b>");
WriteTo(__razor_helper_writer, index);
WriteLiteralTo(__razor_helper_writer, "</b></li>\r\n");
});
}

int numberTen = 10;


string txt = "Kerüld el ha teheted!";

private class InsideClass


{
public InsideClass(string s) { this.MessageTxt = s; }
public string MessageTxt { get; private set; }
}

private HtmlString GetCssClass() {


return new HtmlString("cssoszalynev");
}

public override void Execute()


{
ViewBag.Title = "Inside";
WriteLiteral("\r\n\r\n\r\n");
WriteLiteral("\r\n<h2>Inline Template</h2>\r\n\r\n<ul>\r\n");

for (int i = 1; i < 5; i++) {


Write(ListItemTemplate(i));
}
WriteLiteral("</ul>\r\n\r\n");

Func<dynamic, object> hangsulyos =


item => new System.Web.WebPages.HelperResult(__razor_template_writer =>
{
WriteLiteralTo(__razor_template_writer, "<em>");
WriteTo(__razor_template_writer, item);
WriteLiteralTo(__razor_template_writer, "</em>");
});

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("\r\nA demó eredménye: \"");


Write(ic.MessageTxt);
WriteLiteral("\"<br />\r\nSzám éréke: ");
Write(numberTen);
WriteLiteral("<br />\r\nTanács: ");
Write(txt);

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.

public class _Page_Views_ViewTest_ViewContext_cshtml : System.Web.Mvc.WebViewPage<dynamic>

A WebViewPage funkcionalitását tudjuk bővíteni, ha leszármaztatunk belőle egy új osztályt. A razor


renderelő számára az @inherits kulcsszóval tudjuk jelezni, hogy a View származási láncába beékeltünk
6.7 A View - A View kontextusa 1-150

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.

6.7. A View kontextusa

A View osztályunk tehát a System.Web.Mvc.WebViewPage leszármazottja. A legfontosabb propertyje


a ViewContext. Ez, miként a neve is mondja, a View adat kontextusa. Az ebben elérhető adatok,
nagyjából megegyeznek a kontroller kontextusánál megismertekkel.

 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.

User – A bejelentkezett felhasználó IPrincipal alapadatai.

IsPost – Az aktuális request post HTTP metódussal érkezett-e.

IsAjax – Az aktuális request egy AJAX hívás miatt indult-e el.

Culture és UICulture – Lekérdezhető és be is állítható ezekkel az aktuális requestet feldolgozó szál


kultúrainformációi.

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

RenderSection(string name) – Láttuk a használatát a _Layout mester oldalak belső felépítésénél. A


hívás helyére illeszti be a beágyazott View section eseményét.

DefineSection(string name, SectionWriter action) – A razor @section funkció metódusváltozata. Ezzel


a @section használata nélkül is tudunk írni a 'name' nevű section-be. "Egy kód többet ér ezer szónál"
alapon, íme, egy példa, ami a 'Head' section-be ír közvetlenül egy javascript kódot:

DefineSection("scripts", () => {
WriteLiteral("\r\n<script type=\"text/javascript\">\r\n function showAlert(){" +
"alert(\'Ez a scripts section\');}\r\n" +
"</script>\r\n");
});

Write(HelperResult result) – A HelperResult egy TextWriter paraméterű Action delegate-et hordoz,


ami ezzel a Write metódussal aktivizálódik és a futási eredménye kerül a Write metódus futásának a
helyére.

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.

6.8. Beépített Html helperek

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.

A Html helpereket két fő csoportra tudnám osztani.

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.

@Html.TextBox("HtmlElemNeve", "Az érték ami megjelenik a textboxban")

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

@Html.TextBoxFor(m => m.aModellPropertyNeve)

@this.Html.TextBoxFor(m => m.aModellPropertyNeve)

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.

Ez a fejezet ezekről a Html helperekről és az extension metódusainak felhasználásáról fog szólni.


Azonban már most előre bocsájtom, hogy ahogyan ezeket a helper metódusokat megírták a method
extension lehetőségét kihasználva, úgy számunkra is nyitva áll az út, hogy írjunk továbbiakat vagy
jobbakat. Ezt majd a könyv vége felé a 11.4 –részben meg is nézzük részletesen.

6.8.1. Nyers adatok.

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.:

< &lt;
> &gt;
& &amp;

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.

6.8.2. Hivatkozás. ActionLink és RouteLink

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", "Hlink2")

<a href="/Helper/Hlink2">A 2. oldalra</a>

Az ActionLink-nek egy további paramétere a RouteValue. Ebben lehet összegyűjteni a hivatkozás


összes URL paraméterét (query string). Az alábbi példában csak a „honnan” URL paraméter-t adjuk
meg egy anonymous objektummal.

@Html.ActionLink("A 2. oldalra, URL paraméterrel", "Hlink2", "Helper", new { honnan = "Hlink" }, null)

<a href="/Helper/Hlink2?honnan=Hlink">A 2. oldalra, URL param&#233;terrel</a>

Ebben a példában az URL paramétereket collection initcializer-el állítjuk be. Az eredmény azonos lesz.

@Html.ActionLink("A 2. oldalra, URL paraméterrel RouteValue Dictionary", "Hlink2", "Helper",


new RouteValueDictionary {{"honnan","Hlink"}}, null)

Mint rendes URL paraméter megjelenik a böngésző címsorában: /Helper/Hlink2?honnan=Hlink. A


paramétert az action metódus paraméterként képes fogadni. Az ActionLink nagyon túlterhelt metódus.
Nagyon kevés különbség van a paraméter listákban, ráadásul azok is átfedésben vannak a
típusmentesség miatt. Ezért arra egy kicsit oda kell figyelni, hogy az átadott paramétert miként fogja
értelmezni. Ezért látható a paraméter lista végén a null, hogy egyértelmű legyen melyik metódus
változatot is hívom a példában. Megfontolandó kiírni a paraméter nevét is (routevalues:), ha
bizonytalanok lennénk.

@Html.ActionLink("Szöveg","Hlink2", routeValues: new { honnan = "Hlink" })


6.8 A View - Beépített Html helperek 1-154

Lehetőségünk van HTML attribútumok meghatározására is anonymous objektummal, ami egy elegáns
kódformát ad.

@Html.ActionLink("A 2. oldalra, de új ablakban", "Hlink2", null, new { id = "indexlink", @class =


"linkek", @target = "_blank" })

<a class="linkek" href="/Helper/Hlink2" id="indexlink" target="_blank">A 2. oldalra, de &#250;j ablakban</a>

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.

@Html.RouteLink("A 2. oldalra, URL paraméterrel", "Default", new { action = "Hlink2", controller =


"Helper", honnan = "Hlink" }, null)

<a href="/Helper/Hlink2?honnan=Hlink">A 2. oldalra, URL param&#233;terrel</a>

A hatás kipróbálásához fel kell venni egy új route bejegyzést:

routes.MapRoute(
name: "complains",
url: "complains/{controller}/{action}/{id}",
defaults: new { id = UrlParameter.Optional }
);

Legyen ez a RouteLink paramétereivel:

@Html.RouteLink("A panaszos oldalra", "complains", new { action="New", controller="Incoming"}, null)

A szövegesen megadott "complains" paraméter hivatkozik az azonos nevű route-ra.


A generált <a> tag: <a href="/complains/Incoming/New">A panaszos oldalra</a>

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.

6.8.3. Űrlap. BeginForm

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 Html.BeginForm nagyon hasonlít az ActionLink-re a paraméterei tekintetében, mivel ez is URL-el


operál. Szintén meg lehet adni RouteValues-t és HTML attribútumokat is. Azonban van egy furcsasága,
mivel a <form></form> tag-ek közé szövegek és HTML elemek kerülnek, így ezt nem lehet definiálni
egy darab HTML tag generálásával, ehhez kettő is kell. Emiatt van a Html.BeginForm mellett
Html.EndForm metódus is. A bevált gyakorlat azonban az, hogy a BeginForm-ot using blokkba tesszük.
Úgy trükköztek a framework készítői, hogy a BeginForm statikus metódus egy MvcForm objektumot
ad vissza, ami IDisposable. Amikor a @using {..} blokknak vége, a .Net meghívja a MvcForm Dispose()
metódusát, ahogy egy jó using blokk végén szokás. Erre a MvcForm utolsó leheletéből odapottyant
még egy lezáró </form> tag-et. Elmés.

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

@using (Html.BeginForm("Hform", "Helper", new { id = 1 }, FormMethod.Post, new { id = "form1" }))


{
@:Szöveg: @Html.TextBox("Szoveges")<br />
<input type="submit" />
}

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).

6.8.4. Szövegbevitel. TextBox, TextArea

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.

public ActionResult Hinput()


{
return View(ActionDemoModel.GetModell(1));
}

[HttpPost]
public ActionResult Hinput(int? id, FormCollection fcoll)
{
if (!id.HasValue) return RedirectToAction("Hinput");

var model = ActionDemoModel.GetModell(id.Value);


if (TryUpdateModel(model))
{
return View(model);
}
return View(ActionDemoModel.GetModell(id.Value));
}
15. példakód
6.8 A View - Beépített Html helperek 1-157

A form definíciója hagyományos Html helperekkel:

@using (Html.BeginForm("Hinput", "Helper", new { id = Model.Id }, FormMethod.Post))


{
@:Szöveg: @Html.TextBox("FullName", Model.FullName)<br />

@:Multiline<br />@Html.TextArea("Address", Model.Address, 2, 20, null)<br />

@:Rejtett: @Html.Hidden("FullNameOrig", Model.FullName)<br />


<br />

<input type="submit" />


}
16. példakód

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:

<input id="FullName" name="FullName" type="text" value="Tanuló 1" />

A name mellett az id is felveszi a második paraméter értékét. Lehetőségünk van az id generálást


megváltoztatni az id megadásával, ahogy az ActionLink-nél már láttuk, például HTML attribútumokká
alakuló anonymous osztállyal.

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.TextArea("Address", Model.Address, 2, 20, null)


<textarea rows="2" cols="20" id="Address" name="Address" />

Ott van még a Hidden helper:

@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");

var model = ActionDemoModel.GetModell(id.Value);


model.FullName = FullName;
model.Address = Address;
return View("Hinput", model);
}
6.8 A View - Beépített Html helperek 1-158

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:

@using (Html.BeginForm("Hinput", "Helper", new { id = Model.Id }, FormMethod.Post))


{
@:Szöveg: @Html.TextBoxFor(m => m.FullName)<br />

@:Multiline<br />@Html.TextAreaFor(m => m.Address, 2, 20, null)<br />

@:Rejtett: @Html.HiddenFor(m => m.FullName, new { name = "FullNameOrig", id = "hnev" })


<br />
@:Jelszó: @Html.PasswordFor(m=>m.Address, new {Name = "Jelszo1"})
<br />
<input type="submit" />
}

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:

<input id="hnev" name="FullName" type="hidden" value="Tanuló 1" />

Ha viszont átírjuk nagybetűsre: new { Name = "FullNameOrig", id = "hnev" }, az eredmény olyan


érdekesen fog kinézni, hogy tartalmazni fog egy name-t és egy Name-t is.

<input Name="FullNameOrig" id="hnev" name="FullName" type="hidden" value="Tanuló 1" />

A FullNameOrig elérhető lesz a FormCollection-ban


és action metódus paraméterként is. Ez is a model
binder egy képessége. De legyünk vele óvatosak,
mert lehet, hogy ezt a trükköt nem jól fogja kezelni
minden Androidos cipőfűző beépített böngészője!

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

6.8.5. Label és formázott megjelenítés

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 eredménye és kinézete:

Label: <label for="FullName">Felhaszn&#225;l&#243; n&#233;v</label>


<br />
LabelFor: <label for="FullName">Felhaszn&#225;l&#243; n&#233;v</label>
<br />
LabelFor: <label for="FullName">Teljes n&#233;v</label>
<br />
Value: Teljes név: -Tanuló 1-
<br />
ValueFor: Teljes név: *Tanuló 1*

A Html.Label egy property nevet vár. Display Attribútum nélküli propertynél,


annak nevét adja vissza. Vagy, ahogy már megismertük a DisplayAttribute által meghatározottat, ha
definiáltunk ilyet.

[Display(Name = "FullNameLabel", ResourceType = typeof(Resources.UILabels))]


public string FullName { get; set; }

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

6.8.6. Legördülő és normál lista

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

@using(Html.BeginForm(null,null,new {id=Model.Id}, FormMethod.Post, new {id = "form1"}))


{
@Html.LabelFor(m => m.KeyPurchase)
@Html.DropDownListFor(m => m.KeyPurchase.Id,
new SelectList(Model.PurchasesList, "Id", "ProductName", Model.KeyPurchase.Id))
}
<br />
<input type="submit" value="Ment" form="form1"/>
17. példakód

A HTML5-ös játék kedvéért a submit gombot a formon kívülre helyeztem.

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.

1. IEnumerable képes lista.

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.

A kiszolgáló actionök get-re és post-ra:

[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:

<select id="KeyPurchase_Id" name="KeyPurchase.Id">

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.

A 17. példakódban a View-ban SelectList(this.PurchasesList, "Id", "ProductName",…) segítségével


adtam meg a listaelemek forrását. Ez az a tipikus eset, amikor a kódot legalább a modellbe át kellene
tenni. A modellben van az IEnumerable adatforrás (PurchasesList), és ott vannak a property nevek is.
Itt van a legjobb helye egy metódusban:

public SelectList GetSelectList()


{
return new SelectList(this.PurchasesList, "Id", "ProductName",this.KeyPurchase.Id);
}

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:

Value – az <option> value értéke szövegesen.


Text – az elem felirata
Selected – az elem ki van-e választva.

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:

@Html.DropDownListFor(m => m.KeyPurchase.Id, null)

A kiszolgáló actionben a ViewData tárolóban a célproperty névbejárásával (KeyPurchase.Id) egyező


indexű elemét feltöltöttem egy listával. A háttérben ezt a listát fogja felhasználni a legördülő lista
elemeiként.
6.8 A View - Beépített Html helperek 1-162

[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);
}

Ennek a helpernek van még egy különleges paramétere az optionlabel, aminek a


szerepe, hogy ebből a szövegből készül egy új listaelem, ami a lista elejére kerül,
amolyan null értékként.

@Html.DropDownListFor(m => m.KeyPurchase.Id, null,"Egyik sem")

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:

public int[] KeyPurchaseIds { get; set; }

A View szakasz:

@using (Html.BeginForm("Hlist", null, new { id = Model.Id }, FormMethod.Post, new { id = "form2" }))


{
@Html.LabelFor(m => m.KeyPurchase)
@Html.ListBoxFor(m => m.KeyPurchaseIds,
new SelectList(Model.PurchasesList.ToList(), "Id", "ProductName", Model.KeyPurchaseIds))
<br />
<input type="submit" value="Ment" form="form2"/>
}
A ListBoxFor már bejelölt elemeit szintén listával tudjuk felsorolni (Model.KeyPurchaseIds.ToList()).

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.

6.8.7. Jelölők és rádióvezérlők


CheckBox

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.

@Html.LabelFor(m => m.VIP)


@Html.CheckBoxFor(m => m.VIP)

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.

<label for="VIP">Fontos &#252;gyf&#233;l</label>


<input checked="checked" id="VIP" name="VIP" type="checkbox" value="true" />
<input name="VIP" type="hidden" value="false" />

RadioButton

A rádió gombok kezelését támogatandó, elérhető a RadioButton és a RadioButtonFor párja. Egy


felhasználási példát mutat a következő View szakasz:

@Html.LabelFor(m => m.KeyPurchase)


<ul style="list-style: none">
@foreach (var pitem in Model.PurchasesList)
{
<li>
@Html.RadioButtonFor(m => m.KeyPurchase.Id, pitem.Id,
pitem.Id == Model.KeyPurchase.Id) @pitem.ProductName
</li>
}
</ul>
6.8 A View - Beépített Html helperek 1-164

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.

A modellt kezelő get-post action páros:

[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);
}

6.8.8. Editor és Display template-ek

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; }

Nézzünk valami használhatót is egy példasoron keresztül. A példakódok a TemplateDemoController


felügyelete alatt lesznek megvalósítva és a kipróbáláshoz szükségünk lesz egy modellre, ami az
előzőleg használt ActionDemoModel egy mutánsa.
6.8 A View - Beépített Html helperek 1-165

public class TemplateDemoModel


{
[HiddenInput(DisplayValue = false)]
public int Id { get; set; }

[Display(Name = "FullNameLabel", ResourceType = typeof(Resources.UILabels))]


[DataType(DataType.Text)]
public string FullName { get; set; }

[Display(Name = "Vásárló címe")]


[DataType(DataType.MultilineText)]
public string Address { get; set; }

[Display(Name = "Vásárló email")]


[DataType(DataType.EmailAddress)]
public string Email { get; set; }

[Display(Name = "Vásárlások összértéke")]


public decimal TotalSum { get; set; }

[Display(Name = "Utolsó vásárlás")]


[DataType(DataType.Date)]
public DateTime LastPurchaseDate { get; set; }

[Display(Name = "Vásárlások listája")]


public IList<TemplateDemoProductModel> PurchasesList { get; set; }

[Display(Name = "Kiemelt várárlás")]


public TemplateDemoProductModel KeyPurchase { get; set; }

[Display(Name = "Fontos ügyfél")]


public bool VIP { get; set; }

public static TemplateDemoModel GetModell(int id)


{ //Vissszaadja az id-vel rendelkező példányt, ha nincs csinál egyet
}

public static IList<TemplateDemoModel> GetList(int count)


{ //Az összes eddig létrehozott példányt listázza, ha számuk < count, akkor létrehozza
}

private static Dictionary<int, TemplateDemoModel> datalist;


}
A példakódok között az itt nem kifejtett metódusok is ott vannak természetesen.

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

public class TemplateDemoController : Controller


{
public ActionResult Index()
{
return View(TemplateDemoModel.GetList(5));
}

public ActionResult Details(int id)


{
return View(TemplateDemoModel.GetModell(id));
}

public ActionResult Edit(int id)


{
return View(TemplateDemoModel.GetModell(id));
}

[HttpPost]
public ActionResult Edit(int id, FormCollection coll)
{
var model = TemplateDemoModel.GetModell(id);
if (this.TryUpdateModel(model))
return RedirectToAction("Index");

return View(model);
}
}

Egy jobb klikk a View metóduson és készülhet


a típusos View.

Ennél a dialógusablaknál be kell kapcsolni a „Create a


strongly-typed view” checkboxot és a Model class-t meg
kell adni. A View sablon (Scaffold template) a „List”
legyen. A „Reference script libraries”-t egyelőre
kapcsoljuk ki, mert beindítja a kliens oldali validációt, ami
zavaró lenne a következő példákban. Az Add
megnyomására, létrehoz egy kezdetleges táblázatot,
oszlopfejlécekkel a modell elemei számára. A táblázat
fejlécei DisplayNameFor-al a cellák a DisplayFor-al
segítségével töltődnek ki. A létrejött View-ból kitöröltem
a Delete metódust megcélzó ActionLink-et, mert most
nem érdekes. Ami fontos az a generált View és a
futásának az eredménye, a HTML (a View kód alatt).
Mindkettőt összehúztam, hogy ne foglaljanak sok helyet:

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

Ugyan így le tudjuk generálni a Details metódusból a


hozzá tartozó View-t. A Scaffold template legyen a
Details.

Az Edit View-t az Edit template-el készítjük el. A


létrejött Edit.cshtml-ben a propertykhez a HTML
beviteli mezőket az EditorFor fogja előállítani.

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á.

[Display(Name = "Fontos ügyfél")]


public bool? VIP { get; set; }

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.

Egy lenyitható listát ad, ami


nem lenyitható, mert
„disabled”.
6.8 A View - Beépített Html helperek 1-168

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.

[Display(Name = "Fontos ügyfél")]


[UIHint("Text")]
public bool? VIP { get; set; }

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.

[Display(Name = "Vásárlások összértéke")]


[UIHint("Int32")]
public decimal TotalSum { get; set; }

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.

@Html.EditorFor(model => model.TotalSum,"Int32")

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:

@Html.EditorFor(model => model.TotalSum,"NemlétezőTemplateNév")

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.

Ezek a sablonok a leggyakoribb típusokhoz rendelkezésre állnak az MVC forráskódjában. A kutatást a


System.Web.Mvc.Html.TemplateHelpers osztállyal érdemes kezdeni. Ebben vannak a típus és a típusra
jellemző, HTML-t előállító funkciók gyűjteménye. A sablonok nem cshtml fájlban vannak, hanem
kódból van összeállítva a HTML kimenet. Ezeket az azonos névtérben levő DefaultDisplayTemplates
statikus osztályban találhatjuk meg. Az itt található implementációk tanulmányozása jó kiinduló pont
lehet a .cshtml sablon nélküli editor és display template-ek fejlesztéséhez, amikkel jobb teljesítményt
lehet elérni.

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.

Típus Display (csak megjelenítés) Editor (szerkesztés)


(s)byte, (u)int, (u)long Formázott kimenet <input type="number"
decimal Két tizedes jeggyel <input type="text"
string Html kódolt szöveg <input type="text"
bool <checkbox vagy <select (=bool?) <checkbox vagy <select (=bool?)
disabled=disabled
object, interface Nincs megjelenítés A leszármazott típus editorja, vagy ha
nincs leszármazott, akkor semmi.
System.Drawing.Color Jelenleg nincs speciális template <input type="color"

IEnumerable Lista Lista


("collection")

Itt pedig álljon a DataType enum értékeinek megfeleltetett sablonok listája.

DataType Display (csak megjelenítés) Editor (szerkesztés)


Date Formázott kimenet <input type="date"
DateTime Formázott kimenet <input type="datetime"
EmailAddress <a href=\"mailto: <input type="email"
HTML Formázott kimenet Nincs
MultilineText Formázott kimenet <textarea
Password Nincs megjelenítés <input type="password"
PhoneNumber Formázott kimenet <input type="tel"
Text Html kódolt szöveg <input type="text"
Time Formázott kimenet <input type="time"
Url <a href= <input type="url"

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):

<input id="TotalSum" name="TotalSum" type="text" value="1,3455" /> KEUR

Jelenleg a TemplateController három actionje ezeket produkálja:

Index listanézet (DisplayFor) Detail Edit (EditorFor)


(DisplayFor)

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(""); }

<input type="range" id="@htmlid" name="@Html.Name("")" value="@((int)Model)" data-theme="c" max="400"


min="1" size="5" style="vertical-align: middle;" />

<input type="text" value="@(Model)" id ="inner_@htmlid" disabled="disabled" style="width: 40px;"> EUR

<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

E két helper nagyvonalú leegyszerűsítésnek is felfogható.

<hr />
<div class="editor-fileld">
@Html.EditorForModel()
</div>

Az Edit.cshtml –be írva létrehoz minden egyes propertyhez egy


labelt és egy szerkesztőt, ahogy azt a modell definíciójában
meghatároztam. Ez most nagyjából megegyezik a View-t
varázsló dialógus ablakból generált Edit template eddigi
megjelenésével. A vízszintes elválasztó vonal (<hr />) csak azért
van ott, hogy tisztán látszódjon meddig tartanak az eredeti Edit
template-ben felsorolt vezérlők. A vonal alatt az
EditorForModel eredménye látható. Emlékeztetőnek: a
ScaffoldColumn attribútummal tudunk kizárni propertyket a
dinamikus sablongenerálásból.

EditorFor komplex példa

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:

Az előbbiekben használt modellnek van két propertyje is, amelyek TemplateDemoTermekekModel


típust használnak, amire most lesz majd szükségünk.

[Display(Name = "Vásárlások listája")]


public IList<TemplateDemoProductModel> PurchasesList { get; set; }

[Display(Name = "Kiemelt várárlás")]


public TemplateDemoProductModel KeyPurchase { get; set; }
6.8 A View - Beépített Html helperek 1-173

Itt a modellben szereplő osztály kódja.

public class TemplateDemoProductModel


{
static readonly Random rand = new Random();

[Display(Name = "Azonosító")]
public int Id { get; set; }

[Display(Name = "Cikkszám")]
[DataType(DataType.Text)]
public string ItemNo { get; set; }

[Display(Name = "Termék név")]


[DataType(DataType.Text)]
public string ProductName { 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
}

Legelőször, a szokásos módon hozzuk létre a modellfüggő táblázat


fejléc feliratait egy lista template-tel a DisplayTemplates mappába.
(Helyi menü -> Add -> View)

A View-t varázsló dialógusablakban a mellékelt


beállítássokkal készítessük el a template-et. A nyilak nem
a széljárást jelölik, hanem ennyi helyen kell módosítani
az alapértelmezett beállításokat.

A View neve „TemplateDemoProductModelHeader”. A


létrejött View fájlt kicsit át kell alakítani, hogy a célnak
megfelelő legyen. Ez azt jelenti, hogy jó sok mindent ki
kell törölni, hogy csak ennyi maradjon belőle:
6.8 A View - Beépített Html helperek 1-174

@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.

Ezután egy újabb View készítése következik, de ezt az


EditorTemplates-be kell rakni.

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>

<td> @Html.EditorFor(model => model.ProductName) </td>


<td> @Html.EditorFor(model => model.Quantity) </td>
</tr>

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>

<p> <input type="submit" value="Save" /> </p>

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.

A Save gomb hatására a post requestet fogadó action


FormCollection típusú "coll" paraméterben
megjelennek a táblázat sorai „indexelős” stílusban. Ezt
a formátumot megérti a model binder és feltölti vele a
PurchasesList kollekciót.

Ezt természetesen magunk is összeállíthatjuk az


<input> elemek nevei számára és fel fogja tudni
dolgozni, majd feltölti az azonos nevű
gyűjteményünket. Az indexelős elnevezéséhez
írhattam volna a következő sorokat is az Edit.cshtml-
be, így is azonos eredményt kapunk.

<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ó.

@Html.DisplayFor(m => m.PurchasesList, "TemplateDemoProductModelHeader")


@for (int i = 0; i < Model.PurchasesList.Count; i++)
{
@Html.EditorFor(m => m.PurchasesList[i], "TemplateDemoProductModel", "PurchasesList[" + i + "]")
}

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.DisplayFor(m => m.PurchasesList, "TemplateDemoProductModelHeader")


@{ int i = 0; }
@foreach (var item in Model.PurchasesList)
{
@Html.EditorFor(m => item, "TemplateDemoProductModel", "PurchasesList[" + i++ + "]")
}

6.8.9. Partial és Render Partial

Láttunk példát a Partial View-k használatára pl. a @Html.Partial("DemoPartial") helper metódussal.


Ennek az eredménye egy MvcHtmlString, ahogy az lenni szokott. Amikor a View példány renderelt
kódja fut, akkor ez a MvcHtmlString egy köztes tárolóba kerül, ahonnan majd a responseba. Ez egy kis
időkiesést okoz. Nagy forgalmú web alkalmazásoknál, sok kicsi sokra megy alapon, olyan éles a helyzet,
hogy az ezredmásodpercek is számítanak. A RenderPartial használatával kimarad az MvcHtmlString-re
alakítás és köztes tárolás fázisa. Közvetlenül a response-ba kerül a renderelt partial View eredménye.
Ennek az ára hárommal több karakter, mert felhasználni csak kód blokk razor szintaxissal lehet. (Mivel
nincs MvcHtmlString visszatérési értéke, mint a normál Html helpereknek)

@{ Html.RenderPartial("DetailList", Model.PurchasesList); }

Emlékeztetőül a normál verzió: @Html.Partial("DetailList", Model.PurchasesList)

Ugyan ez a helyzet az Action() és RenderAction helperekkel:

@{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>

@foreach (var item in Model)


{
@Html.EditorFor(m => item, "TemplateDemoProductModel", "PurchasesList[" + i++ + "]")
}
</table>

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

6.8.10. Validációs üzenetek megjelenítése

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(ErrorMessage = "A név megadása kötelező!")]


[CustomValidation(typeof(ValidationDemoModel), "ValidateFullName")]
public string FullName { get; set; }

[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; }

A kontroller actionök az alábbiak:

[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

return View("Hvalid", inputmodel);


}

A futás eredménye Szöveg -> „Tanulo 11”-nél:

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

Nem érdemes összetéveszteni a HtmlHelper.ActionLink metódussal, ami komplett <a> elemet


generál és beállítja a href attribútumát. Emez pedig csak URL stringet generál, amit a HTML kódban
tudunk felhasználni.
6.9 A View - UrlHelper 1-180

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

Az első Url.Action(„AnotherAction”) eredménye a többi felhasználásban is azonos linket generál:

/urlhelper/AnotherAction

Lehetőség van Url paraméterek megadására is. Az alábbi kód eredménye az alatt látható.

A link paraméterekkel: @Url.Action("AnotherAction","UrlHelper",new {oid=1, category="cats",


startindex=50})
A link paraméterekkel: /complains/UrlHelper/AnotherAction?oid=1&category=cats&startindex=50

Relatív URL helyett tudunk generálni teljes URL-t is.

A teljes Url: @Url.Action("AnotherAction","UrlHelper",null,"http","localhost")


A teljes Url: http://localhost:18005/complains/UrlHelper/AnotherAction

A paraméter nélküli változata @Url.Action() az aktuális oldal (request.RawUrl) relatív URL-jét


jelenti.

RouteUrl

Ez pedig a Html.RouteLink párja. A következő példa a RouteLink–el foglalkozó részben használt


„complains” nevű route map bejegyzést felhasználva generál URL-t.

A RouteUrl 1: @Url.RouteUrl("complains", new {action= "New",controller="Incoming"}) <br />


A RouteUrl 2: @Url.RouteUrl("complains", new {action= "New",controller="Incoming"},
Request.Url.Scheme)
A RouteUrl 1: /complains/Incoming/New
A RouteUrl 2: http://localhost:18005/complains/Incoming/New

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.

<img src="/Images/orderedList2.png" />

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.

<img src="@Url.Content("~/Images/orderedList2.png")" />

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.

www.domainnev.com/virtualis -> c:\www\virtualis\

www.domainnev.com/masikkalkalmazas -> d:\websites\amasik\

www.domainnev.com/tovabbialkalmazas -> d:\tovabbiak\harmadikalkalmazas.5.2.1\

www.domainnev.com/alkalmazasmasikgepen -> http://www1.internaldomain.local

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.

 egységes domain név miatt egységes arculatot tükröz az URL.


 elég egy SSL certificate az összes rendszerhez,
 lehetséges azonos autentikációs cookie használata az összes alkalmazás számára (SSO –
egyszeres bejelentkezés az összes rendszerbe),
 stb.

A legegyszerűbb módja a kipróbálásnak, ha az MVC alkalmazásunk projektbeállításait megváltoztatjuk


a „Web” fülön belül.

Állítsuk át „Use Visual Studio Development Server”-re. A portot


18005-re állítottam, de ez nem lényeges. A „Virtual path” mezőbe
írjunk be egy virtuális útvonalat. Azzal, hogy ezt megadtuk a
/Images/orderedList2.png alkalmazáson belüli fizikai fájl
eléréséhez a /virtualis/Images/orderedList2.png URL-t kell
használnunk. Emiatt megváltozik az img elérési út definíciójának az
értelme.
6.9 A View - UrlHelper 1-182

<img src="/Images/orderedList2.png" /> -> Nem lesz elérhető

<img src="@Url.Content("~/Images/orderedList2.png")" /> -> Jól fog működni.

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:

<img src="~/Images/orderedList2.png" />

Ö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.

Link direct: <img src="/Images/orderedList2.png" /><br />


Content: <img src="@Url.Content("~/Images/orderedList2.png")" /> <br />
Link virtual: <img src="~/Images/orderedList2.png" /><br />

A generált HTML részlet:

Link direct: <img src="/Images/orderedList2.png" /><br />


Content: <img src="/virtualis/Images/orderedList2.png" /> <br />
Link virtual: <img src="/virtualis/Images/orderedList2.png" /><br />

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 IsLocalUrl 1: @Url.IsLocalUrl("/complains/UrlHelper/AnotherAction?oid=1&category=cats") <br />


A IsLocalUrl 2: @Url.IsLocalUrl("http://localhost:18005/UrlHelper/AnotherAction") <br />
A IsLocalUrl 3: @Url.IsLocalUrl("/http://index.hu")

Az 1. URL esetén az érték True ez nyilvánvaló.

A 2. URL-re azt mondja, hogy False, azaz nem „local”. Ez is jó.

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

7. Aszinkron üzem, AJAX

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.

Valójában ez a működési forma nagyon sok előnnyel jár.

 Gyorsabb és áttekinthetőbb egy oldalrészlettel foglalkozni, mint az egész oldal összes


jellemzőét figyelemmel kísérni. Minél kevesebb összefüggés van a web oldalon található
elkülönülő részek között, annál valószínűbb, hogy eltérő kontroller-, adat- és modelligénye is
lesz.
 Az oldal egy szeletének újratöltése kevesebb erőforrást, memóriát, processzor műveletet
igényel, mint a teljes oldal újra felépítése. Gondoljunk a mobil eszközök processzoraira.
 Kevesebb a böngészőbe letöltött tartalom, kisebb a sávszélesség igény. (mobil hálózat). Ez egy
grid esetén nagyon szembetűnő. Sok esetben az egyedüli jó megoldás, ha az ilyen gridet
lapozhatóvá tesszük, a lapozást kezelését pedig AJAX hívásokkal biztosítjuk.
 A modellek szétbonthatók kissúlyú, célirányos osztályokra. Amikor az oldalt teljesen újratöltjük
valószínűleg több felesleges adatbázisműveletre lesz szükség olyan oldalrészletek tartalmának
előállításához, amik nem is változtak meg a megelőző percekben. Nagy forgalmú alkalmazásnál
ez komoly szemponttá válik. Az oldalrészlet előállításához elég egy táblarészlet az
adatbázisból.

É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.

7.1. Keretrendszerek tárháza

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.

A JSON 32adatformátum vesszővel elválasztott ”név”:”érték” párokból áll szövegesen.

{ "név": "Blöki", "fajta": "Kuvasz",”oltvavan”:1,”kora”:3 }

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.

7.3. jQuery dióhéjban

A keretrendszer oldala a http://jquery.com/ címen található. Érdemes megnézni a dokumentációját,


mert számos példával illusztrálja a képességeit. Sok-sok könyv, ingyenesen elérhető oktatóanyag
foglalkozik részletesen ezzel, így most csak a leglényegesebbet összegezném a használatából, ami a
későbbi témák, példakódok megértéséhez szükséges lesz.

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

<input id=”azonosito” class=”szovegesmezo” name=”nev” />

beviteli mezőt, a jQuery szelektor és metódushívás így nézhet ki:

$(’#azonosito’).hide(); //Ahol a formátum értelmezése: $(szelektor).metódus();

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();

A jQuery szelektorok és a metódusok is jQuery objektummal térnek vissza. Ennek az a haszna


számunkra, hogy a metódusokat láncolhatjuk. Ez ismerős lehet, hisz a LINQ láncolt metódusait is
hasonlóan tudjuk így használni. Emiatt a következő sor megmutatja és ki is törli a ’szovegesmezo’
osztállyal ellátott HTML input elemek értékét.

$(’.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:

<input id=”azonosito1” class=”szovegesmezo” name=”nev” onClick=”esemenykezelo()”/>


<input id=”azonosito2” class=”szovegesmezo” name=”cim” onClick=”esemenykezelo()”/>
<input id=”azonosito3” class=”szovegesmezo” name=”email” onClick=”esemenykezelo()”/>

Í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:

$(’.szovegesmezo’).on( ’click’, function(event){


$(this).css(’background-color’,’red’);
});
A jelenlegi ajánlás az, hogy ezt az on() metódust használjuk.
7.3 Aszinkron üzem, AJAX - jQuery dióhéjban 1-186

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’);
}
});

Ez az eseményfeliratkozós módszer az unobtrusive 33 megközelítésnek egyik jellemzője javascript


környezetben. Ezt láttuk a validációnál is, amikor a validációs üzenetek ott voltak a HTML mezőhöz
kapcsolva. Az előbb látott, egyébként jól működő eseménykezelésnek van egy olyan hátránya, hogy
minden egyes nevesített vagy azonosított, a selectorral megtalált HTML elemre külön kell feliratkozni.
Most csak a „szovegesmezo” osztállyal ellátott elemekre iratkoztunk fel egy lépésben. Ezzel
megtehetjük, hogy az alkalmazásunk összes textbox-át ellátjuk ezzel az osztállyal és mindenhol
működni fog. Aztán megjelennek majd további CSS osztályok a HTML elemen, amik a design miatt
vannak/lesznek ott. Esetleg a szovegesmezo osztály később CSS stílusokat is meg fog határozni, mert
valaki észreveszi a dizájnnal foglalkozók közül, hogy ott van, és fel fogja használni, mint stílusosztályt.
És ott vagyunk, amit nem akartunk, hogy az eseménykezelés (ami kódolás) és a design teljesen
összekeveredett. Nézzük meg ezt a HTML definíciót:

<a class=”vilagos lekerekitett halvanyulo gomb gomb-ikonnal popup” href=”#”> Ugrás


</a>

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:

<a class=”vilagos lekerekitett gomb gomb-ikonnal popup” href=”#”


data-ui-halvanyulo=”true” data-ui-halvanyulo-sebesseg=”300”> Ugrás </a>

Sőt még az is sejthető, hogy a data-ui-halvanyulo-sebesseg attribútum a halványulás sebességégének


a meghatározása miatt van ott. Az elnevezést konvencióban használva névtereket képezhetünk, amivel
még világosabb leírást adhatunk. A fenti link klikk eseményére a következő jQuery kóddarabbal fel is
iratkozhatunk és egy menetben a sebesség értékét is lekérdezhetjük.

$("a[data-ui-halvanyulo=true]").on("click", function (evt) {


var sebesseg=this.attr(’data-ui-halvanyulo-sebesseg’);
//halványítás, majd ugrás
});

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")
}

A lényeg, hogy a jquery.unobtrusive-ajax.js tartalma valahogy lekerüljön a böngészőbe. A web.config


UnobtrusiveJavaScriptEnabled beállítást false-ra állítva is működni fognak az MVC ajax-os lehetőségei,
de, ilyenkor markup és a használt javascript is más lesz. Ezek fényében kezdjünk el foglalkozni az MVC
AJAX szolgáltatásaival.

7.4. Ajax helperek

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:

@Ajax.ActionLink("Részletek", "Details", new { id = item.Id },


new AjaxOptions()
{
HttpMethod = "get",
InsertionMode = InsertionMode.Replace,
OnBegin = "openPopupDialog",
OnComplete = "closePopup",
UpdateTargetId = "popupdiv"
})

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.

InsertionMode – Az UpdateTargetId által azonosított HTML elemhez képest a sikeresen letöltött


tartalmat pontosan hova helyezze. Replace -> lecseréli a belső tartalmát, tehát a hivatkozott HTML
elemet nem. InsertBefore -> beszúrja elé, InsertAfter -> utána. Ezzel szabályozható, hogy egy
oldaldarab lecserélést vagy bővítést akarunk.

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

LoadingElementDuration – A LoadingElementId által meghatározott elemet a jQuery show


metódusával jeleníti meg. Ennek a metódusnak van egy duration paramétere, ami a nem látható
állapotból a teljes megjelenéséig történő „előtűnés” idejét határozza meg ezredmásodpercben.

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.

OnSuccess – Az UpdateTargetId-vel jelölt tartalom feltöltése után futtatandó eseménykezelő function


neve.

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.

<div id="popupdiv" style="display: none;"></div>

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:

<div id="betoltespopup" style="color: green; display: none;">Betöltés...</div>


7.4 Aszinkron üzem, AJAX - Ajax helperek 1-190

A div-ekből egyszerűen csinálhatunk popup ablakokat, a jQuery-UI kiegészítővel. Mivel ez is része a


normál MVC projekt template-eknek így csak hozzá kell kapcsolnunk az oldalainkhoz:

@section Scripts {
@Scripts.Render("~/bundles/jqueryval")
@Scripts.Render("~/bundles/jqueryui")
}
Az unobtrusive is kell természetesen.

Ahhoz, hogy a folyamat elinduljon, használnunk kell az OnBegin eseménykezelőt:

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:

@{ var ajaxOptions = new AjaxOptions


{
HttpMethod = "Post",
InsertionMode = InsertionMode.Replace,
UpdateTargetId = "updatablelist",
};
}

@using (Ajax.BeginForm("IndexListPartial", null,ajaxOptions,new {id = "adfrom"}))


{
@Html.TextBox("findName")
@Html.TextBox("findAddress")

<input type="submit" value="Keress">


}

<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

Említettem, hogy a generált HTML-ben különbség van a javascript események kezelésében az


UnobtrusiveJavaScriptEnabled web.config beállításától függően. Ha nem használjuk az unobtrusive
lehetőségeket, (false) a fenti form markupja valahogy így néz ki:

<form action="/ajaxdemo/IndexListPartial" id="adfrom" method="post"


onclick="Sys.Mvc.AsyncForm.handleClick(this, new Sys.UI.DomEvent(event));"
onsubmit="Sys.Mvc.AsyncForm.handleSubmit(this, new Sys.UI.DomEvent(event), { insertionMode:
Sys.Mvc.InsertionMode.replace, httpMethod: &#39;Post&#39;, loadingElementId: &#39;betoltes&#39;,
updateTargetId: &#39;updatablelist&#39;, onBegin: Function.createDelegate(this, ClearErrors),
onComplete: Function.createDelegate(this, AttachToSelectorClick), onFailure:
Function.createDelegate(this, SetError) });">

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

<form action="/ajaxdemo/IndexListPartial" data-ajax="true" data-ajax-begin="ClearErrors" data-ajax-


complete="AttachToSelectorClick" data-ajax-failure="SetError" data-ajax-loading="#betoltes" data-ajax-
method="Post" data-ajax-mode="replace" data-ajax-update="#updatablelist" id="adfrom" method="post">

7.5. Ajax helperek demó

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.

Ez a terv. Kezdjük lebontani fő egységekre.


7.5 Aszinkron üzem, AJAX - Ajax helperek demó 1-193

 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.

Röviden ennyit a célokról. A teljes megvalósítást a példakódban az AjaxDemoController –t követve


megtaláljuk. Következzenek a részletek és magyarázatok.

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:

<link type="text/css" href="~/Content/themes/base/jquery-ui.css" rel="stylesheet" />


<style type="text/css">
.selector.ui-icon { border: medium dotted #ddd;}
.selector.ui-icon:hover {border: medium dotted red;}
.selector.selected { border: medium solid red; }

#detaillist {margin-top: 10px;}


table {width: 100%;}
th {border-bottom: 2px solid;}
td {border: 1px solid #ddd; }
td:first-child { border-left:none;}
td:last-child { border-right:none;}
.tablecol1 {width: 55px;}
.tablecol2,.tablecol3 {width: 350px;}

</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",
};
}

<h2>Ügyfelek listája (@DateTime.Now.ToString("yyyy.MM.dd hh:mm:ss.fff"))</h2>

@using (Ajax.BeginForm("IndexListPartial", null,ajaxOptions,new {id = "adfrom"}))


{
<table class="ui-tabs">
<colgroup>
<col class="tablecol1" />
<col class="tablecol2" />
<col class="tablecol3" />
<col/>
</colgroup>
<tr>
<th>#</th>
<th>@Html.DisplayNameFor(model => model.FullName)</th>
<th>@Html.DisplayNameFor(model => model.Address)</th>
<th></th>
</tr>

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

<div id="betoltespopup" style="color: green; display: none;">Betöltés...</div>

<div id="popupdiv" style="display: none;"></div>


18. példakód

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

A következő kód az IndexListPartial.cshtml partial View tartalma.

@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>

Ez az ügyféllista megjelenítéséért felel a sorkiválasztóval és az ajax linkekkel. A sorkiválasztó a selector-


os div. A további class-ok a Jquery UI-ban definiált jobbra nyíl ikon megjelenítéséhez kellenek. Alul a
detaillist div-be kerül a kiválasztott ügyfélhez tartozó terméklista, ha a selector-ra kattintunk.

A demóalkalmazásban az egész eseménykezelés egy JS blokkban került implementálásra (amit valós


helyzetben érdemes kiemelni egy .js fájlba). Ezt most blokkonként értelmezzük.

Az alábbi szakasszal feliratkozunk a dokumentum betöltésekor bekövetkező eseményre, egy


anonymous funkcióval.

<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 selid változót egy menetben is megszerezhettem volna, ha így írom:


var selid = $(this).addClass('selected').closest("tr").attr('data-itemid');

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

Ezután az ActionLink-nél látott dialógusablak kezelése következik.

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.

Az, hogy az aktuális ügyfélsort szerkesztjük (Ajax.ActionLink("Szerkesztés", "Edit",…) vagy csak


megnézzük a részleteket (Ajax.ActionLink("Részletek", "Details",…) csak az action neve alapján különül
el, az ablak és eseménykezelés azonos. A Details.cshtml-t nem másolom ide, mert teljesen egyszerű.
Megjelenik és az ablak bezárható. Az Edit.cshtml lényegesebb. A Layout=null miatt ez partial View lesz.
Az AjaxOptions-ba tettem egy mentés megerősítést kérő üzenetet. A szerkesztő input mezőket szintén
ajax formba ágyaztam. Így be tudom mutatni, hogy egy eredetileg Ajax.ActionLink által indított ajax
lekérdezés eredményével feltöltött jQuery dialógus ablak tartalma is kezelhető Ajax.BeginForm
helperrel.
7.5 Aszinkron üzem, AJAX - Ajax helperek demó 1-198

@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>

@Html.HiddenFor(model => model.Id)

(itt voltak a mező szerkesztők…)

<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)

public class AjaxDemoController : Controller


{

private void LongTimeDBAccess()


{
System.Threading.Thread.Sleep(2000);
}

public ActionResult Index()


{
var model = TemplateDemoModel.GetList(100).Take(10);
return View(model.ToList());
}
22. példakód

A következő action kezeli le a keresés form post eseményét. A metódusparaméterek az oszlopok


kereső textbox-jainak a névszerinti megfelelői. Amikor szűrjük a listát és volt(ak) szűrési feltétel(ek),
akkor két lehetőség van: Vannak a szűrésnek megfelelő sorok, akkor megy a lista a partial View
segítségével. Ha nincs egy sor sem, akkor megy egy hamis HTTP hibába csomagolt üzenet, hogy „Nincs
találat” (SendNotFound metódussal). Ez persze nem a legszebb kezelési mód, de arra jó, hogy
kipróbáljuk az ajax form hibakezelési mechanizmusát. Így a kereső formnak át tudunk üzenni. Az
üzenetet annak AjaxOptions OnFailure = "SetError" definíciója miatt a 20. példakód által mutatott
kódban fel tudunk használni, hogy megjelenítsük.

[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;
}

if (filtered) //volt szűrési feltétel


{
var list = query.ToList();
if (list.Count > 0) //van keresési eredmény.
return PartialView(list);

this.SendNotFount();
return null;
}
return PartialView(TemplateDemoModel.GetList().Take(10).ToList());
}

private void SendNotFount()


{
Response.StatusCode = (int)HttpStatusCode.NotFound;
Response.Write("Nincs találat");
Response.End();
}
23. példakód

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.

public ActionResult Details(int id)


{
this.LongTimeDBAccess();
return PartialView(TemplateDemoModel.GetModell(id));
}

public ActionResult Edit(int id)


{
this.LongTimeDBAccess();
return PartialView(TemplateDemoModel.GetModell(id));
}

[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

7.6. JSON adatcsere

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.

JSON adatok a kiszolgálótól

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.

A böngésző oldali működés alapja a jQuery UI autocomplete 36 beépített képessége. A használata


nagyon egyszerű. A szelektorral ki kell választani a textbox-ot, amit ezzel a képességgel szeretnénk
felruházni és az autocomplete metódus paraméterében meg kell adni az adatforrást, ami szolgáltatja
a választható elemeket. Az adatforrás tekintetében három opciót is támogat a jQuery UI autocomplete.

 Megadhatunk egy JS tömböt.


 Megadhatunk egy URL-t, ahonnan egy JSON válaszban várjuk a begépelt karaktereknek
megfelelő listát. Ez általában jónak tűnik, de most nem ezt fogjuk használni, mert az URL-t
dinamikusan kéne generálni mind a két kereső textbox-hoz. Egyszerű használni:
$("input").autocomplete({ source: "/autocompleteUrl”) });

36
http://jqueryui.com/autocomplete/. API dokumentáció: http://api.jqueryui.com/autocomplete/
7.6 Aszinkron üzem, AJAX - JSON adatcsere 1-202

 Megadhatunk egy JS funkciót, amivel kézben tarthatjuk az adatlekérés műveletét, és


elvégezhetjük a számunkra szükséges JSON adatok lekérését. A példa számára ez most jó lesz,
mert az eseményt kiváltó textbox-ról majd meg tudjuk szerezni, hogy melyik oszlophoz
tartozik. (data-completefield attribútum)

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

}); //end each

}); //end ready


//További kódok a termékek lista kezeléséhez

Az AttachToSelectorClick után, aminek a funkcionalitása megmaradt, következik a kiválasztás. A


kiválasztás során a jQuery megkeresi az összes olyan input elemet, aminek van data-completefield
attribútuma. Ezzel fogjuk jelezni, hogy melyik textbox legyen autocomplete képes és egyben ennek az
attribútumnak az értéke mutatja, hogy melyik adatmező elemeire szeretnénk szűrési feltételt
alkalmazni (melyik oszlophoz tartozik a textbox). A kiválasztás eredményén (két elemet fog találni)
végigiterálunk (.each) és minden egyes megtalált textbox-ot autocomplete képességűvé teszünk. Az
autocomplete metódusnak egy anonymous objektumban átadhatjuk a paramétereket. Most két
paramétert állítunk be. A minLength által szabályozzuk, hogy a felhasználónak minimálisan két
karaktert kell begépelnie, hogy elinduljon a keresés. A source számára egy funkcióban biztosítjuk a
találati listát. Ez a funkció két paramétert kap, az egyik a request, aminek csak egy „term” tulajdonsága
van. Ez tartalmazza a felhasználó által beírt (2 vagy több) karaktert. A response paraméter egy callback
funkciót takar. Ezt a funkciót kell meghívni a begépelt karaktereknek megfelelő találati listával. És ez
nem is bonyolult, mert a jQuery.getJSON metódusának a harmadik paramétere pont egy ilyen callback-
ot vár. Így csak össze kell drótozni a kettőt. A getJSON első paramétere az URL, amit szolgáltatja a listát,
itt most az AutoComplete action. A második paramétere egy JS objektum, amiből query string lesz.

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.

<th><input name="FullName" type="text" data-completefield="findName" placeholder="Név szűrés"></th>


<th><input name="Address" type="text" data-completefield="findAddress" placeholder="Cím szűrés"></th>

De módosíthatjuk is a Html.TextBox-okat is, kiegészítve a HTML attribútumokat előállító anonymous


objektumokkal. Mire kell ilyenkor figyelni? Arra, hogy az anonymous objektum property nevében a
kötőjeleket aláhúzással helyettesítjük:

@Html.TextBox("FullName",null,new {data_completefield= "findName"})


7.6 Aszinkron üzem, AJAX - JSON adatcsere 1-203

A lényeg, hogy legyen beállított data-completefield attribútuma 37 . A kontrollert csak egyetlen


metódussal kell bővíteni, ami kiszolgálja a getJSON adatigényét:

[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:

return new JsonResult() { Data = response, JsonRequestBehavior = JsonRequestBehavior.AllowGet };

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);
}

A JsonRequestBehavior.DenyGet el is hagyható, mert ez a default érték. Számunkra most az egészből

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.

A response fülön látható a visszaérkező JSON adat:

["Vásárló 11","Vásárló 110","Vásárló 111","Vásárló 112","Vásárló 113","Vásárló 114","Vásárló


115","Vásárló 116","Vásárló 117","Vásárló 118","Vásárló 119","Vásárló 211"]
7.6 Aszinkron üzem, AJAX - JSON adatcsere 1-205

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ó.

Van még néhány további tulajdonsága is.

 MaxJsonLength – Ezzel korlátozhatjuk a JSON szöveg hosszát. Ez alapértelmezetten 2Mbyte.


Ha átlépjük a határt egy InvalidOperationException -t kapunk ajándékba.

 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.

 ContentType – Ezt ritkán érdemes állítani. Az alapértelmezett "application/json" megfelelő.

A Data tulajdonságban megadott objektumot egy JavaScriptSerializer alakítja JSON szabványú


szöveggé. Ennek semmi köze nincs a .NET sorosításhoz, teljesen saját metodikát alkalmaz. Nem is az
MVC framework része. A System.Web.Extensions.dll-ben található. Két hasznos metódusa van: a
Serialize() és a Deserialize(). A háttérben a JsonResult a Serialize metódust használva készíti el a JSON
választ. A sorosítás és a visszaalakítás általában jól szokott működni. A string, char, boolean, Guid, Uri
és numerikus típusokat jól képes kezelni. A felsorolásokat (amit már láttuk a példában), a dictionary-t,
HashTable szintén feldolgozza. Az enumokat nem szövegesen, hanem az alaptípus (byte, int, long)
megfelelőjeként numerikussá alakítja. Az Flag típusú enumokat vesszővel elválasztott numerikus
értékek JS tömbjévé formázza. A teljes konverziós viselkedési lista az MSDN-en 38 fellelhető. Az olyan
publikus propertyket, amiket ki akarunk zárni a JSON sorosításból a ScriptIgnoreAttribute-al tudjuk
megjelölni a sorosítandó osztályon.

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.

Az AjaxJSONDemoController-ben két új actiont hoztam létre. Az Index csak a View-hoz kell. A


GetServerData pedig egy JsonResult objektumot küld vissza, egy anonymous objektumban két
dátummal és egy haszontalan szöveggel. Ez egy nagyon hasznos dolog, hogy a JSON adat
létrehozásához nincs szükség hagyományos osztályra, hanem egy ilyen objektum is megfelelő.

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

public ActionResult ServerData()


{
return View();
}

public ActionResult GetServerData()


{
var sorositando = new
{
Message ="Szerver idő",
CurrentTime = DateTime.Now,
EniacFinished = new DateTime(1946, 2, 14),
};

return Json(sorositando, JsonRequestBehavior.AllowGet);


}

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.

@Html.ActionLink("Szerver adatok nyersen", "GetServerData")


<br /><hr />

A válasz, benne a nem normális dátumokkal:

{"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.

@Ajax.ActionLink("Szerver adatok visszaalakítva", "GetServerData", new AjaxOptions()


{
OnSuccess = "ParseJson",
UpdateTargetId = string.Empty
}
)
<br /><br />

<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);

var parsed = $.parseJSON(ajaxXHR.responseText);

$('#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>

A lényegi átalakítás amúgy a ParseDate funkcióban van, ami a /Date(1367778850927)/ formátumot


olvashatóvá teszi. Természetesen szükség van erre is:

@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:

public class DateTimeJsonConverter : JavaScriptConverter


{
public override IEnumerable<Type> SupportedTypes
{
get {return new[] { typeof(DateTime) };}

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

public override IDictionary<string, object> Serialize(object obj, JavaScriptSerializer serializer)


{
if (!(obj is DateTime)) return null;

DateTime datet = (DateTime)obj;


var result = new Dictionary<string, object>();

//A JS objektum tulajdonságai


result["dateTime"] = datet.ToString();
result["date"] = datet.ToShortDateString();
result["long"] = datet.ToString("D");
return result;
}

public override object Deserialize(IDictionary<string, object> dictionary, Type type,


JavaScriptSerializer serializer)
{
string dateString;
if (dictionary.ContainsKey("dateTime"))
{
dateString = dictionary["dateTime"].ToString();
}
else if (dictionary.ContainsKey("date"))
{
dateString = dictionary["date"].ToString();
}
else if (dictionary.ContainsKey("long"))
{
dateString = dictionary["long"].ToString();
}
else return null;

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:

{"dateTime":"2013.05.05. 22:17:05","date":"2013.05.05.","long":"2013. május 5."},

É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.

Egy új action, hogy ne zavarja az előzőt:

public ActionResult GetServerData2()


{
var sorositando = new
{
Message = "Szerver idő",
CurrentTime = DateTime.Now,
EniacFinished = new DateTime(1946, 2, 14),
};

JavaScriptSerializer serializer = new JavaScriptSerializer();


serializer.RegisterConverters(new[] { new Infrastructure.DateTimeJsonConverter() });

var serialized = serializer.Serialize(sorositando);

HttpResponseBase response = this.HttpContext.Response;


response.ContentType = "application/json";
response.Write(serialized);
response.End();
return null;
}
7.6 Aszinkron üzem, AJAX - JSON adatcsere 1-209

A JavaScriptSerializer-t manuálisan példányosítjuk és beregisztráljuk a saját konverterünket. Ezek után


használjuk a JSON sorosítót és a response-t, amit szintén manuálisan töltünk fel ennek szöveges JSON
eredményével. Így csinálja a JsonResult osztály is, csak azt most nem tudjuk használni, mert nem tud a
saját konverterünkről.

A ServerData.cshtml View-t kiegészítettem az új actiont használó linkkel és a hozzá tartozó JS


funkcióval:

@Ajax.ActionLink("Szerver adatok visszaalakítva saját DateTime konverterrel", "GetServerData2", new


AjaxOptions()
{
OnSuccess = "ParseJson2",
UpdateTargetId = string.Empty
})

function ParseJson2(json, status, ajaxXHR) {


$('#szovegph').html(json.Message);
$('#szerveridoph').html(json.CurrentTime.dateTime);
$('#eniacfinishph').html(json.EniacFinished.date + '<br />' + json.EniacFinished.long);
}

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

Saját Action Result JSON-hoz.

Eddig csak a beépített ActionResult leszármazottakat használtunk az actionök visszatérési


értékeiként. A GetServerData2 action végén található JSON sorosítást megoldó kóddarabból
könnyűszerrel csinálhatunk saját, újrahasznosítható ActionResult megvalósítást.

public class MyJsonResult : ActionResult


{
private readonly object _model;

public MyJsonResult(object modeltoJson)


{
this._model = modeltoJson;
}

public override void ExecuteResult(ControllerContext context)


{
var serializer = new JavaScriptSerializer();
serializer.RegisterConverters(new[] { new DateTimeJsonConverter() });

var serialized = serializer.Serialize(_model);

HttpResponseBase response = context.HttpContext.Response;


response.ContentType = "application/json";
response.Write(serialized);
response.End();
}
}

Mindössze arra van szükség, hogy az ExecuteResult, felűlbírálható metódusba helyezzük át a


sorosítást végző kódot. Ezek után már használhatjuk is egy action visszatérési értékeként:

public ActionResult GetServerData2()


{
var sorositando = new
{
Message = "Szerver idő",
CurrentTime = DateTime.Now,
EniacFinished = new DateTime(1946, 2, 14),
};

return new MyJsonResult(sorositando);


}

JSON adatok küldése a kiszolgálónak.

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.

public class AjaxPostAttribute : FilterAttribute, IAuthorizationFilter


{
public void OnAuthorization(AuthorizationContext filterContext)
{
HttpRequestBase request = filterContext.RequestContext.HttpContext.Request;
string actionname = filterContext.RouteData.GetRequiredString("action");
if (request.HttpMethod.ToLowerInvariant() != "post" || !request.IsAjaxRequest())
{
throw new InvalidOperationException(actionname + " csak AJAX POST requesttel hívható!");
}
}
}
7.6 Aszinkron üzem, AJAX - JSON adatcsere 1-211

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 használt modell is rém egyszerű, talán az Internal propertyje teszi érdekessé:

public class MyJsonModell


{
public int Id { get; set; }
public string Message { get; set; }
public DateTime ClientUTCTime { get; set; }

public MyJsonModell Internal { get; set; }


}

A View ezzel foglalkozó szelete egy táblázat, hogy legyen hova írni az eredményeket:

<a onclick="SendJsonData()">JSON objektum küldése a szervernek</a>


<br />
<table>
<colgroup>
<col style="width:150px"/>
<col style="width:200px"/>
<col style="width:200px"/>
</colgroup>
<tr>
<th></th><th> küldés előtt</th><th> válasz </th></tr>
<tr>
<th>Id</th> <td id="sendId"></td> <td id="recId"></td>
</tr>
<tr>
<th>Idő</th> <td id="sendIdo"></td> <td id="recIdo"></td>
</tr>
</table>

Az első sorban levő linkre kattintva az alábbi JS funkció indul el.

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.

Az Id eggyel nőtt. A dátum 1200 nappal és 2


órával későbbi. Az óra eltérés oka, hogy a
javascriptből a 0. időzóna (GMT) szerinti idő
került megjelenítésre, majd elküldésre. Ezt a
formátumot felismeri a JavaScriptSerializer.

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.

7.7. Az MVVM keretrendszerekről néhány szóban

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:

 Kliens oldali megjelenítő. (Jelenleg a HTML maga.)


 Kliens oldali modell. Amik most a HTML elemekből kiszedett adatok voltak.
 A megjelenítő és a modell összekapcsolása: Bind. (Ezt csináltuk Id-k alapján.)
7.7 Aszinkron üzem, AJAX - Az MVVM keretrendszerekről néhány szóban 1-213

 A modell változásai és más események kliens oldali kezelése.


 Adatkonverziók. (A dateTime probléma.)
 JSON sorosítás adat továbbítás.

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.

Az ilyen keretrendszerek valamilyen egyedi template rendszerben gondolkodnak. A HTML elemeket


névkonvenció alapján lehet ellátni attribútumokkal. A modell és az attribútumokkal felparaméterezett
HTML mezők között kétirányú adatkötést valósítanak meg. A modellben eseménykezelőket
határozhatunk meg.

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ó.

Név Alap keretrendszer Szerver oldal


Angularjs Nincs Nincs
Backbone.js jQuery Nincs
Kendo UI jQuery Igen, MVC is.
Knockout.js Nincs Nincs
Knockoutmvc Nincs Igen, MVC

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:

public class CategoryModel : ICategoryFullNameUpdateModel


{
public int Id { get; set; }

public string FullName { get; set; }

public DateTime CreatedDate { get; set; }

[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; }

[Display(Name = "Kategória pár")]


public CategoryModel JoinedCategory { get; set; }
. . .
}
A modell még további részleteket is tartalmaz, amit a példakódban lehet megnézni. A modell
példányosítása és az „SubCategories” hierarchikus feltöltése is ott szerepel. Az WillNeverValid property
és az ICategoryFullNameUpdateModel interfész használata csak később kerül elő. A példa kódokat a
BinderDemoController vezérli.
8.1 A model binder - Egyszerű típusok és a beépített lehetőségek 1-216

8.1. Egyszerű típusok és a beépített lehetőségek

Kezdjük ott, hogy a request megérkezik szerverre és az ASP.NET + MVC összeállítja a Request
objektumot.

 Amennyiben GET volt a metódus, rendelkezésre állhatnak az URL paraméterek név-érték


párjai. Kitüntetett szerepet kap, ha a route definícióban jelenlevő Id is szerepel az URL-ben,
mert akkor ez is külön érték (/controller/action/1?category=cats). Illetve bármilyen más
nevesített paraméter, nem csak az 'id'.

 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;

if (!Int32.TryParse(Request["Id"], out id))


return Content("Id nem áll rendelkezésre");
var model = CategoryModel.GetCategory(id);

string fullName = Request["FullName"];


if (string.IsNullOrEmpty(fullName))
return Content("A nevet meg kell adni!");
model.FullName = fullName;

if (DateTime.TryParse(Request.Form["CreatedDate"], out createdDate))


model.CreatedDate = createdDate;
else
return Content("'Létrehozva' nem dátum");

return RedirectToAction("Index");
}

A post adatokat kétféleképpen is elérhetjük a requestből. Az egyik a Request[”inputNev”], a másik a


Request.Form[”inputNev”] forma. Ez utóbbi csak a form input mezőket tartalmazó szűkített lista.
Gondolom észrevehető, hogy ez a manuális, egyedi property feltöltés nagyon kényelmetlen, időrabló.
Ráadásul karbantarthatatlan kódot eredményez.

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:

 Gyors, mert az adattípus konverziók pontosan illeszkednek a modell propertykhez.


 Csak azzal a propertyvel foglalkozunk, amihez kódot implementáltunk, amiben célzott
adatkonverziót végzünk.

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 Request objektumból név alapján kivenni a szükséges string értékeket


 Típus konverziót végrehajtani a string értékből, és feltölteni a modell propertyjeit.
 Validálni az adatokat.

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.

public ActionResult Edit3(CategoryModel inputmodel, string fullname,


string createdDate, DateTime CreatedDate)

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.

public ActionResult Edit3(CategoryModel inputmodel, string fullname, string createdDate)

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");
}

Az exception kezelés kikerülhető és egy if-es szerkezetté alakítható a TryUpdateModel metódussal:

[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.

Adatbázis alapú entitás Egyik modell Másik modell


id id id
UserName UserName
Email Email
Last Last Last

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;
}

//2. white list


if (this.TryUpdateModel(model, string.Empty, new[] { "FullName" }))
{
//MindenOk.
bool isValid = this.ModelState.IsValid;
}

//3. black list


if (this.TryUpdateModel(model, string.Empty, null, new[] { "CreatedDate", "WillNeverValid" }))
{
//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:

public interface ICategoryFullNameUpdateModel


{
string FullName { get; set; }
}

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.

A negyedik megoldás a Bind attribútum használatára épül:

[HttpPost]
[ValidateOnlyFormFieldsAttribute]
public ActionResult Edit7(
[Bind(Include = "FullName", Exclude = "CreatedDate,WillNeverValid")] CategoryModel inputmodel)
{
var model = CategoryModel.GetCategory(inputmodel.Id);
model.FullName = inputmodel.FullName;

bool isValid = this.ModelState.IsValid;


8.1 A model binder - Egyszerű típusok és a beépített lehetőségek 1-222

return RedirectToAction("Index");
}

Mint látható, metódusparaméterként várjuk a CategoryModel típusú példányban a form adatokat. A


Bind attribútum Include és Exclude tulajdonságaival lehet szabályozni, hogy mikkel foglalkozzon a mb.
A kettő tulajdonság közül természetesen elég az egyiket használni. Mindkét tulajdonságban több
property nevet is fel lehet sorolni vesszővel elválasztva.

Egy további lehetőség, hogy használhatjuk a modell propertyn a ReadOnlyAttribute vagy az


EditableAttribute-ot. Ezzel az adott modell propertyt kivonjuk a default model binder fennhatósága
alól. Tipikusan jól használható a példamodellben is létező createdDate dátum típusú tulajdonság
esetén feltételezve azt, hogy az adott entitás létrehozásakor állítódik be a createdDate, és a felhasználó
soha sem módosíthatja azt. (Üzleti objektumoknál elég gyakori, hogy tartalmaznak createdDate,
createdUser, modifiedDate, modifiedUser jellegű tulajdonságokat, amiket a háttérben automatikusan
töltenek ki). Mivel ez egy attribútum, így globális hatással lesz az összes bindolási igényre nézve, ami a
modellel kapcsolatban történik.

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

public class ValidateOnlyFormFieldsAttribute : ActionFilterAttribute


{
public override void OnActionExecuting(ActionExecutingContext fc)
{
var modelState = fc.Controller.ViewData.ModelState;

var keysWithNoIncomingValue =
modelState.Keys.Where(x => !fc.Controller.ValueProvider.ContainsPrefix(x));

foreach (var key in keysWithNoIncomingValue)


modelState[key].Errors.Clear();
}
}

Használata:

[HttpPost]
[ValidateOnlyFormFieldsAttribute]
public ActionResult Edit7([Bind(Include = "FullName", Exclude = "CreatedDate")] CategoryModel
inputmodel)
{
var model = CategoryModel.GetCategory(inputmodel.Id);
model.FullName = inputmodel.FullName;

bool isValid = this.ModelState.IsValid; //True lesz

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.

8.2. Felsorolások, listák és szótárak

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.

public ActionResult ListTree()


{
ViewData["depth"] = 1;
return View(CategoryModel.GetList());
}

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.

private static List<CategoryModel> CreateListInner(int itemNumber, int deep)


{
if (itemNumber <= 0) return new List<CategoryModel>();
var result = new List<CategoryModel>(itemNumber);
for (int i = 1; i < itemNumber + 1; i++)
{
var id = tid++;
var cm = new CategoryModel
{
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(cm);
}

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>

Mivel a DisplayFor egy CategoryModel.cshtml-t vár a Views/BinderDemo/DisplayTemplates


mappában, ezért erre is szükség lesz:

@using MvcApplication1.Models
@model CategoryModel
@{ int index = 0; int depth = (int)ViewData["depth"];}

<tr style="border-bottom: 1px solid #555">


<td>
@Html.DisplayFor(model => model.Id)
</td>
<td>
@Html.ActionLink(Model.FullName, "EditTree", new { id = Model.Id })
8.2 A model binder - Felsorolások, listák és szótárak 1-225

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

A template alapján előáll a CategoryModel propertyjeinek a megjelenítése és szintén tartalmaz egy


belső iterációt az SubCategories további megjelenítésére. Talán a futási eredmény segíti a megértést:

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:

public ActionResult EditTree(int id)


{
ViewData["depth"] = 1;
return View(CategoryModel.GetCategory(id));
}
8.2 A model binder - Felsorolások, listák és szótárak 1-226

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)

Ennek az eredménye, egy ilyen input mező elnevezés lesz:

<input id="JoinedCategory_FullName" name="JoinedCategory.FullName" type="text" value="">

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.

Ami érdekes, hogy listaelemeknél a ListaNév[index].PropertyNév konvenciót követve tudunk olyan


szerkeszthető propertykkel rendelkező listákat készíteni, amit a default model binder megért. A
FullName property vonatkozásában továbbkövetve a logikát, listák listáit is tudjuk így reprezentálni:

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:

@foreach (var item in Model.SubCategories)


{
@Html.DisplayFor(m => item, "CategoryModel", "SubCategories[" + index++ + "]",
new { depth = depth + 1 })
}

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:

@for (index = 0; index < Model.SubCategories.Count; index++)


{
@Html.EditorFor(m => m.SubCategories[index], new { depth = depth + 1 })
}

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.

Egy elem beállítása és a dictionaryhöz adása így néz ki:

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.

<tr style="border-bottom: 1px solid #555">


<td>
@Html.DisplayFor(model => model.Id)
@Html.HiddenFor(model => model.Id)
@Html.Hidden("key", "Di"+Model.Id)
</td>
<td class="editor-field">
@Html.EditorFor(model => model.FullName)
</td>
<td class="editor-field">
@Html.EditorFor(model => model.CreatedDate)
</td>
</tr>
<tr>
<td colspan="3">
@if (Model.SubCategories != null)
{
<div style="padding: 8px; margin-left:
12px;@CategoryDictionaryModel.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)
{
8.2 A model binder - Felsorolások, listák és szótárak 1-229

@Html.EditorFor(m => item.Value, "CategoryDictionaryModel",


"SubCategories[" + item.Key + "]", new { depth = depth + 1 })
}
</table>
</div>
}
</td>
</tr>

A HTML markupban látszanak az dictionary kulcs alapján elnevezett input mezők.

<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

8.3. Bonyolult modellek problémái

Most elmélkedjünk egy kicsit a modellosztályunkról, de ne ilyen egyszerűről, mint ez a CategoryModel,


hanem valami robosztusabbról. Egy olyanról, aminek sok-sok propertyje van, amiben bőségesen van
üzleti logika megvalósítva a normál validáción felül is. Említettem a modellek összetettségének
tárgyalásánál, a 4.2 fejezetben, hogy mik az előnyei és hátrányai az egyszerű (POCO) és a komoly üzleti
logikával felruházott modelleknek. Mivel akkor még nem volt értelme a model binder szempontjait is
figyelembe venni, így most vizsgáljuk meg röviden, egy elképzelt nagyon összetett modell esetét.
Képzeletben, abban a szituációban vagyunk, amikor a request megérkezik és az action modellosztály
típusú paramétert vár a model binder-rel feltöltve.

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

8.4. Mélyen belül

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ő:

public interface IModelBinder


{
object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext);
}

A paraméterei közül a ControllerContext tartalmazza az aktuális request adatokat a HttpContext-et, a


route adatokat és a többit, amit a kontrollerrel foglalkozó fejezetben megnéztünk. A
ModelBindingContext pedig a modellel kapcsolatos adatokat, mint például a validációs állapotot, a
modell meta adatait (jórész az attribútumokból képzett kivonatot), és a modellhez illeszkedő konverter
készletet. A visszatérési értéke pedig maga a felépített modell.

A különféle helyzetekre speciális binder osztályok valósítják meg beépítetten az IModelBinder


interfészt:

 HttpPostedFileBaseModelBinder
 ByteArrayModelBinder
 LinqBinaryModelBinder
 CancellationTokenModelBinder
 FormCollectionModelBinder
 DefaultModelBinder

Az előző részekben eddig csak a Default- és a FormCollectionModelBinder-t használtuk. A Controller


osztályon található a Binders nevű, ModelBinderDictionary típusú property a működés főszereplője,
ami a modell/action számára igényelt típusú adathoz legjobban illeszkedő IModelBinder
megvalósításokat tárolja.

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

FormCollectionModelBinder aktivizálódik, mert rendelkezik ilyen attribútummal. Más szóval,


a FormCollection típust a FormCollectionModelBinder fogja feltölteni.
4. Ha egyik próbálkozás sem „jött be”, következik a DefaultModelBinder. Az eddigi példákban
jórészt ez lépett működésbe, amikor modellt vártunk az action paramétereként.

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)

public class CategoryModelBinder : IModelBinder


{
public object BindModel(ControllerContext controllerContext,
ModelBindingContext bindingContext)
{
HttpRequestBase request = controllerContext.HttpContext.Request;
int id;
string fullName;
DateTime createdDate;

id = Convert.ToInt32(request.Form.Get("Id"));
fullName = request.Form.Get("FullName");
createdDate = Convert.ToDateTime(request.Form.Get("CreatedDate"));

var model = CategoryModel.GetCategory(id);


model.FullName = fullName;
model.CreatedDate = createdDate;
return model;
}
}

Lehetőségünk van használni a ValueProvider-t is (a dőltbetűs szakasz helyett), aminek a feladata,


hogy az adatot konvertálja és validálja:

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.

private void Application_Start()


{
//…
ModelBinders.Binders.Add(typeof(CategoryModel), new CategoryModelBinder());
//…
}

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)

private void Application_Start()


{
//…
ModelBinderProviders.BinderProviders.Add(new CategoryModelBinderProvider());
//…
}

A provider megvalósítása is nagyon egyszerű. Mindössze a GetBinder metódusban egy IModelBinder-


t megvalósító osztállyal kell visszatérnünk, amennyiben a paraméterként érkező típushoz tudunk
kínálni megfelelő bindert, és null-al pedig akkor, ha nem.

public class CategoryModelBinderProvider : IModelBinderProvider


{
public IModelBinder GetBinder(Type modelType)
{
if (!typeof(CategoryModel).IsAssignableFrom(modelType))
return null;

return new CategoryModelBinder();


}
}

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)

A harmadik módszerben a CustomModelBinderAttribute leszármazottal tudjuk megjelölni a


modellosztályunkat, előidézve ezzel azt, hogy az attribútumban példányosított binder változat fog
dolgozni a modellünkkel. Ekkor természetesen nincs szükség a global.asax bővítésére. Ezzel minden
egyes modellünk számára saját bindert tudunk írni. Természetesen a modell lehet a saját maga bindere
is, ha megvalósítja az interfészt. Láttunk ehhez hasonló példát a CustomValidation attribútum
használatánál, ahol a modell önmagának volt a validátor osztálya.

public class CategoryModelBinderAttribute : CustomModelBinderAttribute


{
public override IModelBinder GetBinder()
{
return new CategoryModelBinder();
}
}
8.4 A model binder - Mélyen belül 1-235

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.

Még számtalan egyedi eset létezhet, amikor jól jöhet ez az ismeret.

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.

A ValueProvider rendszer, hasonlóan a model binder-hez, egy gyűjteményben tárolja a rendelkezésre


álló speciális megvalósításokat szolgáltató példányokat. Ez azonban egy picit összetettebb. A
működésre kész megvalósításokat a ValueProviderFactories.Factories statikus propertyje szállítja egy
feltöltött ValueProviderFactoryCollection formájában. Ebben a gyűjteményben vannak a
rendelkezésre álló provider szolgáltatók bejegyezve, amiknek az elnevezése nagyon beszédesre
sikerült.

 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.

A fenti lista egyben az adatszolgáltatásra tett próbálkozások sorrendje is. A JsonValueProviderFactory


csak akkor jelenik meg a műveleti sorban, ha az aktuális request AJAX alapon érkezett. Ennek jótékony
hatását már láttuk a JSON-nal foglalkozó fejezetben. A felsorolt …ValueProviderFactory-k egy
IValueProvider interfészt megvalósító osztályt adnak vissza. Ezeknek az osztályoknak a nevei
szerencsére megegyeznek a …Factory osztályok neveivel a „Factory” utótag nélkül. Nos, ezek a
…ValueProvider-ek azok, amik felhasználhatóak a GetValue(string key) metódusukon keresztül arra,
hogy a key által meghatározott értéket egy ValueProviderResult osztályba csomagolva megkapjuk.
Ebből a csomagból aztán, annak metódusaival, az igényelt típusra konvertálva el tudjuk kérni a típusos
értéket. A konkrét …ValueProvider kontroller kontextus függően, a

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.

1. A provider listában szerepel a RouteDataValueProvider (mivel a


RouteDataValueProviderFactory szolgáltatja). Ezek szerint a ValueProvider tud adatokat
szolgáltatni a route bejegyzésekből is:

string action = bindingContext.ValueProvider.GetValue("action").AttemptedValue;


string controller = bindingContext.ValueProvider.GetValue("controller").AttemptedValue;

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.

2. A másik probléma amivel megtéveszthetjük magunkat, ha formon is szerepel az Id mint hidden


mező és a form action attribútumában is, mint URL paraméter. A providerek sorrendjéből
látható, hogy a hiddenben tárolt Id értéke fog győzni, és az alábbi példában az Id=99999
elveszik, mint URL paraméter.

@using (Html.BeginForm("EditCategModelBinder","BinderDemo", new {Id="99999"}, FormMethod.Post))


8.4 A model binder - Mélyen belül 1-237

{
@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:

@using (Html.BeginForm("EditCategModelBinder", "BinderDemo",


new { Id = "99999", WillNeverValid ="Hát ez honnan jött?"}, FormMethod.Post))
{
@Html.Partial("EditPartial", Model)
}

A post URL-je így nézett ki:


/BinderDemo/EditCategModelBinder/99999?WillNeverValid=Hát%20ez%20honnan%20jött%
3F

Tehát megjelent az Id mellett az WillNeverValid kulcs-érték pár is. A CategoryModelBinder-be


helyezzük el a következőt:

string WillNeverValid = bindingContext.ValueProvider.GetValue("WillNeverValid").AttemptedValue;

Természetesen megjelenik az adat, mert a QueryStringValueProvider szolgáltatja azt. Ezzel a


trükkel a default model binder is megvezethető, mert az is ezt a ValueProvider készletet
használja. Ezek szerint még az input mezőkkel sem szükséges bíbelődi. Legyünk tehát
óvatosak!

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:

@using (Html.BeginForm("FixValueProvider", "BinderDemo",


new { Id = "99999", WillNeverValid = "Ez query string lesz" }, FormMethod.Post))
{
<span>Id:</span> <input name="Id" value="1" readonly="readonly" /> <br />
<span>EzNemValid:</span> <input name="WillNeverValid" value="de nem ám" readonly="readonly"/>
<p>
<input type="submit" value="Save" />
</p>
}

A lényeg kiemelve: az „Id” és az „WillNeverValid” két helyen is szerepel.

 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);

ValueProviderResult action = querystringValues.GetValue("action"); //action=null


ValueProviderResult controller = querystringValues.GetValue("controller"); //controller=null
ValueProviderResult idResult = querystringValues.GetValue("Id"); //idResult=null

int id = (int)routeValues.GetValue("Id").ConvertTo(typeof(int)); //idResult=99999


string WillNeverValid = querystringValues.GetValue("WillNeverValid").AttemptedValue;

var model = CategoryModel.GetCategory(1);


//A model.WillNeverValid értéke a TryUpdaModel után: "Ez query string lesz"
this.TryUpdateModel<CategoryModel>(model, string.Empty, querystringValues);

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.

A ValueProvider-ekről érdemes tudni még néhány dolgot.

 A konkrét típusú ValueProvider tartalmazni fogja a rá vonatkozó request adatokat kivonatolva.


Például a FormValueProvider az input mezőket, a QueryStringValueProvider az URL
paramétereket. Ez azt jeleni, hogy nem a GetValue meghívásakor kezd el keresni a request
adatokban. Jelentheti azt is, hogy feleslegesen kerül feltöltésre, ha később nem is használjuk.
 Az adatkonverziót - amennyiben van értelme - a futó szál kultúrainformációja alapján végzi. Ez
felülbírálható a ConvertTo(Type type, CultureInfo info) második paraméterével.
 A konverzió során a validációk is megtörténnek. Ezt elkerülendő, lehetőség van a
GetValue("kulcs", skipValidation: true) metódusváltozat használatára.
 Lehetőség van csoportosított módon kezelni a bejövő adatokat, ha a kulcsot prefixszel látjuk
el. Leginkább a form input mezőinél vehetjük ennek hasznát. Az MVC is ezt alkalmazza, amikor
modellbe ágyazott komplex típusú propertyt használunk (amikor az input mező elnevezése
PropertyNév.PropertyNév.PropertyNév szerint történik).

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:

<input name="submodel.PropertyI" value="szöveg 1" />


<input name="submodel.PropertyII" value="szöveg 2"/>
8.4 A model binder - Mélyen belül 1-239

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:

var formValues = new FormValueProvider(this.ControllerContext);


this.TryUpdateModel<ImportantModel>(model.Submodel, "submodel", formValues);

Természetesen a ValueProvider-ek és ValueProviderFactory-k listája is bővíthető, és tudunk speciális


megvalósításokat készíteni. Erre egyébként találunk példákat a Microsoft.Web.Mvc névtérben. Van itt
például egy CookieValueProviderFactory, amivel a cookie-ban tárolt értékeket tudjuk elérni és
bindoltatni. Vagy ott van a SessionValueProviderFactory, ami a session bejegyzések eléréséhez, és a
párja a TempDataValueProviderFactory a TempData adatainak eléréséhez és bindolásához
használható. Sőt van itt egy igazi különlegesség is, a ServerVariablesValueProviderFactory, amivel a
webszerver változóit tudjuk modellhez rendelni.

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

9. A biztonság és az értelmes adatok

A téma sokrétű, és sok gondolkozást igényel. Felkavarja a szépen megálmodott rendszertervünket,


annyira távol van a lényegi kitűzött céltól. Nem igaz? A következő alfejezet jó lesz gondolatébresztőnek,
és néhány megoldási javaslatszilánk bemutatását célozza meg. Majd következik néhány további
fejezetet, ami az MVC által nyújtott biztonsági megoldásokat mutatja be.

9.1. A rendszer biztonsága

Emberileg is az egyik legsúlyosabb fájdalom, ha becsapnak, megvezetnek minket. Mire felnövünk,


számos védelmi módszert sajátítunk el a lelki integritásunk megőrzésére. Eltelik 15-20 év, mire azt
mondhatjuk, hogy a várható pszichikai támadási formákra fel vagyunk készülve. Nincs ez máshogy a
webes rendszereknél sem. Az elmúlt 10 év, de különösen az elmúlt időszak ilyen-olyan indíttatású
hacker támadásai csak nyomatékosítják azt, hogy egy nyilvános site készítőjének nem elég az üzleti
intelligenciára odafigyelnie és hibátlanul implementálnia, oda kell figyelnie azokra is, akik az
intelligenciájukat a mi rendszerünk becsapására, feltörésére csiszolgatják. A támadó-védekező játék
„fejlesztői” egymás hibáiból és gyengeségeiből tanulnak. Bár a leggyakoribb támadási formák évek óta
alig változnak, fontos hangsúlyozni, hogy bármilyen rendszernél (internet és intranet is!) a tervezéskor
figyelembe kell venni a bejövő adatok érvényességének alapos vizsgálatát. Számos könyv foglalkozik a
webes rendszerek biztonságával így itt csak azokat a legfontosabb elemeket mutatom be, amelyik az
MVC keretrendszerrel kapcsolatban szóba jöhet, és/vagy azokra megoldást nyújt.

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 egy sajátossága, hogy az alkalmazásunk megvalósítását a lehető legnagyobb


mértékben a kezünk ügyére bízza. Azon kívül, hogy a kiírandó szövegek tartalmát automatikusan
biztonságos HTML formába hozza - és így nem tudunk csak úgy <script> tagek között kódot
rendereltetni egy Html helperrel vagy a @: razor forma után - sok további védelmet nem szolgáltat.
Ráadásul ez is könnyen átléphető a Html.Raw használatával. Az autentikációt, validációt és a request
érvényesítését nem kényszeríti ránk, ezért mindezek csak akkor működnek, ha erről gondoskodunk.
Ezt fontos szem előtt tartani, főleg ha előtte ASP.NET Web Forms környezetben szereztünk
tapasztalatokat, ahol a védelem magasabb szinten alapértelmezett. Ott a ViewState-től kezdve az
eseménykezelős requestig mindenre figyel valami a háttérben.

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

ezt az interfészt megvalósítják, majd sorban meghívogatja az OnAuthorization metódusukat. Ennek


megfelelően, hogyha ilyen szintű védelmet szeretnénk csinálni, akkor két feltételnek kell megfelelni:
az attribútum a FilterAttribute leszármazottja legyen, és mellette valósítsa meg az IAuthorizationFilter
interfészt. Egy további példával illusztrálva, szabályozhatóvá tehetjük, hogy egy actiont csak egy
bizonyos típusú böngészővel lehessen elérni:

[BrowserOnly("Chrome")]
public ActionResult CsakChrome()
{
return Content("Hello Chrome!");
}

public class BrowserOnlyAttribute : FilterAttribute, IAuthorizationFilter


{
private readonly string _browserName;

public BrowserOnlyAttribute(string browserName)


{
this._browserName = browserName;
}

public void OnAuthorization(AuthorizationContext filterContext)


{
if (filterContext.HttpContext == null) return;
if (filterContext.HttpContext.Request.Browser.Browser != _browserName)
throw new InvalidOperationException(
"Ez az action csak " + _browserName + " böngészővel érhető el!");
}
}
Természetesen nem kell ennyire drasztikusnak lenni, lehetne egy http redirect-tel is válaszolni és
átirányítani a többi böngésző számára készült actionhöz.

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:

@using (Html.BeginForm("AntiForgeryServed", "Security"))


{
@Html.AntiForgeryToken()
<input type="submit" value=" Ment " />
}
És az actionre illeszteni az ellenőrző attribútum párját:

[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

public ActionResult AntiForgeryServed()


{
return Content("Minden rendben");
}

A Html.AntiForgeryToken() helper hidden input mezőt készít egy erős kóddal. Valami ilyet:

<input name="__RequestVerificationToken" type="hidden"


value="Z3Ttr2EX7TOnEcwYqpXa7Izuonnh_xjvsc0WMgdFyXN24Lm7bD61U3QEG62BdTE8HUQZQECuw88mS5rqnrzBa__zrr_EKlRc
Wtv4T3erj_DZCowulj3Afa9WeELRZZ-l1xPdblD1qjaY3PklGmvVoJaPb6OcAfwaen-c_cY6Atk1" />

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ő.

A biztonság fokozásához lehetőségünk van az AntiForgery rendszert konfigurálni a statikus


System.Web.Helpers.AntiForgeryConfig tároló osztályon keresztül. A lehallgatás kivédésére
előírhatjuk, hogy csak titkosított csatornán legyen üzemeltethető:

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";

Ezeken kívül bővíthetjük a tokenbe kerülő adatot egy IAntiForgeryAdditionalDataProvider –el:

System.Web.Helpers.AntiForgeryConfig.AdditionalDataProvider =
new MyAntiForgeryAdditionalDataProvider();
9.2 A biztonság és az értelmes adatok - A frontvonal 1-244

Az MVC 3-ban még volt lehetőség a Html.AntiForgeryToken(string salt) változatát használva


„megsózni” a titkosított adatot. Ennek helyét vette át az AdditionalDataProvider. Ez érdekes
lehetőségeket rejt. Az alábbi fapados megvalósítás egy statikus szöveggel bővíti a tokent, amit csak
akkor tekint érvényesnek, ha visszakapja azt a bejövő post requesttel:

public class MyAntiForgeryAdditionalDataProvider :


System.Web.Helpers.IAntiForgeryAdditionalDataProvider
{
public string GetAdditionalData(HttpContextBase context)
{
return "Sós mókus";
}

public bool ValidateAdditionalData(HttpContextBase context, string additionalData)


{
return additionalData == "Sós mókus";
}
}

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.

public class OnlyLastAFDataProvider : System.Web.Helpers.IAntiForgeryAdditionalDataProvider


{
public string GetAdditionalData(HttpContextBase context)
{
object pO = context.Session["PageIdentity"];
if (pO == null || !(pO is int))
pO = 0;
int pIdentity = (int)pO + 1;
context.Session["PageIdentity"] = pIdentity;
return pIdentity.ToString();
}

public bool ValidateAdditionalData(HttpContextBase context, string additionalData)


{
if (string.IsNullOrEmpty(additionalData)) return false;
object pO = context.Session["PageIdentity"];
if (pO == null || !(pO is int)) return false;
int pIdentity = (int)pO;
int addIdentity;
return Int32.TryParse(additionalData, out addIdentity) && pIdentity == addIdentity;
}
}

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

9.3. Felhasználó hitelesítés

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.

 URL alapú hitelesítés


Az azonosítási kód az URL-be van ágyazva. Ez leginkább az egyszer felhasználható aktiváló
linkek vagy jelszó visszaállítási linkek formájában jelentkezik (ma már). Az MVC beépítetten ezt
a módozatot nem támogatja, ha ilyet akarunk, akkor nekünk kell megvalósítani.
 Alapszintű, web form alapú hitelesítés
Interneten jelenlévő oldalak esetén még mindig ez a leggyakoribb. Felhasználói név/email cím
és jelszó párost várnak.
 Windows hitelesítés
Kizárólag intranetes webalkalmazásoknál használatos, ahol a közös hitelesítési szolgáltatót
(Active Directory) a webszerver és a kliens is eléri.
 Certificate alapú hitelesítés
Ez nem tipikusan humán bejelentkezés számára használatos módszer. Jellemzőbb, amikor a
webalkalmazásunk egy távoli (web)szolgáltatásba hitelesíti be magát.
 Webszolgáltatás által nyújtott hitelesítés, Claim alapú hitelesítés
A nagy közösségi és tartalomszolgáltatók által nyújtott lehetőség, hogy a náluk regisztrált
felhasználókat más rendszerekbe is hitelesíteni tudják. Ez hasonlít a Windows hitelesítésre
annyiban, hogy a hitelesítési szolgáltató kulcspozícióban van a hálózat szempontjából. (Ki
állítaná, hogy a Google vagy a Facebook nincs kulcspozícióban az interneten?)

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.

9.3.1. Form alapú hitelesítés

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?

A nyitóoldal, ahogy eddig is láttuk nem igényel semmilyen hitelesítést.

A jobb felső részen levő „Log in” linkre kattintva a bejelentkezési


oldalra kerülünk. Tegyük fel, hogy még nincs accountunk a
rendszerben, ezért regisztráljuk magunkat a „Register” linken
keresztül. Itt megadunk egy nevet és egy legalább hat karakter
hosszú jelszót kétszer, ahogy az kell. Ezzel be is jelentkeztet minket
a rendszerbe, és a jobb felső sarokban
ott lesz a regisztrációs oldalon
megadott nevünk.

A „Log off”-ra bökve


kijelentkezhetünk. A név eltűnik a jobb felső sarokból, helyette újra ott
lesz a Register és a Log in. Ezek után be tudunk jelentkezni a
rendszerbe a Log in oldalon:

Itt lehetőség van a „Remember me?” jelölővel meghatározni, hogy


a böngésző bezárása esetén is megőrizze a bejelentkezési
állapotunkat, így ha újra elővesszük az oldalt egy időhatáron belül,
akkor nem kell újra név+jelszóval bejelentkezni.
9.3 A biztonság és az értelmes adatok - Felhasználó hitelesítés 1-247

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

A hitelesítés részletes beállítását a web.config-ban lehet megtenni. Erre szolgál az <authentication>


elem. Az alábbi beállítás kieszközli az előbb látott tokennel bővített URL formátumot a
cookieless="UseUri" attribútum miatt:

<system.web>
<authentication mode="Forms">
<forms loginUrl="~/Account/Login" timeout="2880" cookieless="UseUri"/>
</authentication>

A többi attribútum jelentése:

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);
}

// If we got this far, something failed, redisplay form


ModelState.AddModelError("", "The user name or password provided is incorrect.");
return View(model);
}

}

Az első, amivel találkozik az MVC feldolgozó motorja, az az Authorize attribútum magán az


AccountController osztályon. Ezzel a kontroller összes actionje számára előírtuk, hogy csak hitelesített
felhasználók érhetik el. Ezt használhatnánk a global filterek között is és akkor a teljes alkalmazásunk
összes actionje csak hitelesített felhasználók számára lesz elérhető. Mivel bejelentkezni a nem
hitelesített felhasználók szoktak, ezért egy lyukat kell ütni a pajzson az AllowAnonymous
attribútummal. Ezzel kivehetjük az adott actiont a magasabb szinten előírt hitelesítés kényszerből. A
Login action egy returnUrl paramétert elfogad, ahova majd a bejelentkezés után visszadobja a
felhasználót. Fel szeretném hívni a figyelmet arra, hogy ez a „returnUrl” kényelmi szolgáltatás
egyébként egy masszív biztonsági kockázat, és nem csak az MVC-nél, hanem bármilyen webes
alkalmazásnál (ld. Open redirection). Itt most a returnURL a form action attribútumban jelenik meg a
Login.cshtml-ben, mint URL paraméter. Ezt a kliens oldalon lecserélve, közvetetten akár, de eltéríthető
a böngésző egy nem kívánt oldalra is. Emiatt van egy hasznos példadarabka az ActionController
osztályban, amit érdemes megszívlelni és átvinni már elkészült régebbi MVC (2-3) projektekbe is:

private ActionResult RedirectToLocal(string returnUrl)


{
if (Url.IsLocalUrl(returnUrl))
{
return Redirect(returnUrl);
}
else
{
return RedirectToAction("Index", "Home");
}
}

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.

A valódi hitelesítést a háttérben a MembershipProvider absztrakt osztály SimpleMembership nevű


megvalósítása végzi a WebMatrix esetében. Természetesen ez is egy jelentős bővítési pont, mivel elég
valószínű, hogy nem lehet minden alkalmazás számára egy darab általános és tökéletes hitelesítési
rendszert készíteni. Vannak azonban közös jellemzők, erre ad alapot a MembershipProvider. Íme,
néhány jellemzője, tulajdonsága és metódusa, ami megmutatja, hogy igen szerteágazó lehetőségünk
van arra, hogy speciális providert építsünk az alkalmazásunknak.

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...

Ha visszamegyünk az Account kontrollerhez és megnézzük a LogOff, Register, Manage actionöket


láthatjuk, hogy a statikus WebSecurity által biztosított metódusokkal kezelhetjük le a felhasználói
fiókokkal kapcsolatos teendőket. Ha statikus az osztály, akkor valahol inicializálni kell. Emiatt van az
AccountController osztályon az InitializeSimpleMembership filter attribútum, aminek a kód fájlja a
Filter mappában van. Ebben az attribútumban egy beállító osztályt határoztak meg, ami biztosítja az
adatbázis hátteret a felhasználói adatok tárolására. Érdemes ezt megnézni, hogy láthassuk, mit hova
és miért tesz a hitelesítési szolgáltatás.
9.3 A biztonság és az értelmes adatok - Felhasználó hitelesítés 1-251

private class SimpleMembershipInitializer


{
public SimpleMembershipInitializer()
{
Database.SetInitializer<UsersContext>(null);
try {
using (var context = new UsersContext()) {
if (!context.Database.Exists())
{
// Create the SimpleMembership database without Entity Framework migration schema
((IObjectContextAdapter)context).ObjectContext.CreateDatabase();
}
}

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);
}
}
}

A profil adatokhoz a modellt az Entity Framework (EF) biztosítja ebben a megvalósításban. A


Database.SetInitializer<UsersContext>(null) és az utána következő using blokk azt fogja
eredményezni, hogy az EF létrehozza az adatbázist és benne a UserContextben lévő UserProfile
entitásosztály által meghatározott azonos nevű táblát is, ha még nem léteznének. (code first) Az
adatbázis kapcsolathoz szükséges connection string a web.config elején található:

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

Természetesen átírhatjuk, hogy más adatbázisra mutasson, de az alapbeállítás eredménye, hogy


létrejött egy adatbázis fájl a projekt App_Data mappájában és ebbe került bele a nemrégiben
regisztrált felhasználó adatai. Még egy kicsit visszatérve a beállító osztályra, a tényleges inicializálást
ez a sor végzi el:

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;

A háttérben szolgáltató SimpleMembership provider egy saját sémában tárolja a bejelentkezési


adatokat a connection string által meghatározott adatbázisban.

Itt látható a létrejött adatbázis. Felül ott van a UserProfile


táblánk. A SimpleMebership táblák egy prefixet kaptak,
ami nem változtatható meg. A Membership tárolja a
felhasználó lényeges hitelesítési adatait. A Roles a
jogosultság csoportokat. A UsersInRoles pedig ez utóbbi
kettő közti több-a-többhöz kapcsoló tábla. A UserProfile és
a Membership is rendelkezik UserId nevű és int típusú
elsődleges kulccsal, ami 1:1 kapcsolatot biztosít a két tábla
között.

Az ábrán nem látszik de a UserProfile tábla UserName


mező típusa nvarchar(max), ami nagyon felesleges méret
egy bejelentkezési névhez. Emiatt a UserProfile modellnél
célszerű a hozzá tartozó propertyn a mezőhosszúságot
szabályozni a StringLength attribútummal:

[Table("UserProfile")]
public class UserProfile
{
[Key]
[DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
public int UserId { get; set; }
[StringLength(60)]
public string UserName { get; set; }
}

A WebSecurity további képességeket is rejt néhány metóduson keresztül

UserExists(string userName) Ezzel ellenőrizhető, hogy a felhasználónév már foglalt-e. Amolyan


előzetes validációként a regisztráció folyamán.

GeneratePasswordResetToken(string userName, int tokenExpirationInMinutesFromNow = 1440)

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.

CreateAccount(string felhasználóiNév , string jelszó, bool kellJóváhagyásiToken) metódussal úgy


tudunk felhasználót regisztrálni, hogy egy visszaigazoló tokent is kérünk. Ez szintén kimehet egy
9.3 A biztonság és az értelmes adatok - Felhasználó hitelesítés 1-253

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.

A RequireRoles(params string[] roles) –t meghívva a szerepek/csoportok neveivel, vad módon


ellenőrizhető, hogy a bejelentkezett felhasználó tagja-e a felsorolt csoportoknak. Ha valamelyikben
nem szerepel, akkor a response-ba egy 401-es (nincs hitelesítve) hibaüzenet fog menni a böngészőnek.

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.

SimpleRoleProvider simpleRoles = Roles.Provider as SimpleRoleProvider;

A csoportok kezeléséhez a SimpleRoleProvider biztosít metódusokat, és nem nehéz, hogy erre


megírjuk a kezelőfelületet. Néhány hasznos metódusa:

void CreateRole(string roleName)


bool RoleExists(string roleName)
bool DeleteRole(string roleName, bool throwOnPopulatedRole)
string[] GetAllRoles()
string[] GetRolesForUser(string username)
string[] GetUsersInRole(string roleName)
bool IsUserInRole(string username, string roleName)
void AddUsersToRoles(string[] usernames, string[] roleNames)
void RemoveUsersFromRoles(string[] usernames, string[] roleNames)

A példakódban az AccountController végén, a SimpleRole régióban írtam néhány actiont és hozzá a


View-kat, hogy látható legyen a SimpleRoleProvider felhasználása. Tényleg nem bonyolult. Egy actiont
emelnék csak ki ezekből, ami a role átnevezéséhez használható. Ebben látható a metódusok jórészének
a használata.

[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");
}

Az átnevezéshez ugyanis nincs metódusa a SimpleRoleProvider-nek, emiatt marad a kapcsolódó


felhasználók átmentése egy új role-ba, mint lehetőség. A DeleteRole második paramétere egy bool,
ami ha true és a role-hoz vannak felhasználók kapcsolva, akkor egy exceptiont dob, hogy ne lehessen
véletlenül aktív role-t törölni. Ez itt most nem fontos, false, azaz törölhető. Ami látható és fontos
sajátosság, hogy membership provider-ek a role-okat és a felhasználókat is név és nem Id alapján
kezelik. Ezt figyelembe kell venni, mert emiatt nem lehet a rendszerben két azonos nevű felhasználó.

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

esetben is átléphetünk a membership providerek adta lehetőségeken, megkerülve azokat.


Megkerülve, mert belsőleg számos esetben azok is ezzel a FormsAuthentication osztállyal operálnak.
Megemlítenék néhány gyakrabban használt metódust.

SetAuthCookie(”userName”, isPersistCookie) – Beállítja a felhasználó (név – és nem Id) számára a


névre szóló hitelesítési cookie-t. Az isPersistCookie-val lehet szabályozni azt, hogy session vagy
maradandó cookie legyen. A „Remember me” checkbox értéke végül ide érkezik meg.

GetAuthCookie() – Létrehoz egy új cookie-t, de azt nem küldi ki a böngészőnek, szemben a


SetAuthCookie-val.

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.

RedirectToLoginPage() – Miként a neve is mondja, egy mozdulattal a böngészőt a bejelentkezési


oldalra irányítja.

RedirectToLoginPage(string extraQueryString) – Az előbbi párja, de egy kiegészítő query stringet is


hozzáfűzhetünk a redirect responsba. (returnURL)

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:

[Authorize(Roles = "Adminok, Menedzserek")]

Sőt lemehetünk felhasználói szintre és azt is megszabhatjuk, hogy csak a felsorolt felhasználók
férhessenek hozzá:

[Authorize(Users = "LocalAdmin, Admin")]

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.

public class AnotherAuthorizeAttribute : AuthorizeAttribute


{

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

public override void OnAuthorization(AuthorizationContext filterContext)


{
if (filterContext == null) {
throw new ArgumentNullException("filterContext");
}

var roles = SplitString(Roles);


IPrincipal user = filterContext.HttpContext.User;
if (user.Identity.IsAuthenticated && roles.Length > 0 && !roles.Any(user.IsInRole))
{
var url = new UrlHelper(filterContext.RequestContext);
filterContext.Result = new RedirectResult(
url.Action("YourAreNotInRole", "Security", new {rolenames = Roles}));
return;
}
base.OnAuthorization(filterContext);
}

private static string[] SplitString(string original)


{
if (String.IsNullOrEmpty(original)) return new string[0];

var split = from piece in original.Split(',')


let trimmed = piece.Trim()
where !String.IsNullOrEmpty(trimmed) select trimmed;
return split.ToArray();
}
}

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.

IPrincipal user = filterContext.HttpContext.User;


bool inrole = user.IsInRole("rolenév");
string username = user.Identity.Name;
bool loggedin = user.Identity.IsAuthenticated;

Nagyjából ennyire képes a form alapú hitelesítés és ekkora a támogatottsága az MVC részéről.

9.3.2. Windows alapú hitelesítés

A hitelesítésről ebben az esetben egy Windows szerveren futó Active Directory/tartományvezérlő


gondoskodik. A tartományvezérlő egyik feladata, hogy az intranet rendszerben (tipikusan vállalati
belső hálózat) egylépéses (Single-Sign-On) bejelentkeztetéssel és hitelesítéssel a hálózat
erőforrásaihoz (fájl megosztásokhoz, nyomtatókhoz, és alkalmazásokhoz, levelezőhöz) való
hozzáférést központilag szabályozza. A bejelentkezés tehát csak egyszer történik meg, amikor a
munkaállomáson begépeljük a tartományi felhasználói nevünket és jelszavunkat (vagy smartcard,
stb.). Az alkalmazásunknak nem kell tehát se bejelentkezési neveket se jelszavakat tárolnia, már csak
a felhasználó egyéb profil adataival kell foglalkoznia, de akár támaszkodhat az Active Directory-ban
tárolt adatokra is (teljes név, email cím, telefonszám, szervezeti egység mind rendelkezésre áll az AD-
ban). Megjegyzendő, hogy egy intranetes webalkalmazás elérhetővé tehető a tartományon kívül jövő
kérések számára is és akkor a tartományi bejelentkezési adatokat a böngészőben előugró dialógus
ablakban kell megadni. Azonban ez a hálózat biztonsága érdekében messze elkerülendő megoldás.
Ahogy változtak az idők ennek a bejelentkezési módnak több nevet is adtak, emiatt a szakirodalomban
fellelhető nevek, mint például Windows authentication, Integrated authentication ugyan azt jelentik.
9.3 A biztonság és az értelmes adatok - Felhasználó hitelesítés 1-256

Ezt a hitelesítési módot az alkalmazás gyökér web.config-jában lehet bekapcsolni az authetntication


mode attribútumban.

<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.

A legegyszerűbben úgy tudjuk ezt a módot kipróbálni és


megnézni mit és hogyan kell állítani a szerveren, ha létrehozunk
egy új Intranet Application projektet. File->New->Project MVC
Web Application és a template-ek közül kiválasztva az említett
sablont, létrehozzuk az új projektet.

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á.

Az IIS Express, ami a VS2012-essel együtt települő


webszerver, ebben a VS verzióban az alapértelmezett
fejlesztői szerver. Ezt a leírás szerint a projekt
tulajdonságok között lehet beállítani. Ez szokott
félreértéseket okozni, mert két „Projekt Property” is
van. Az egyik, amit elérhetünk a projekt -> jobb klikk ->
(a menülistában legalul) Properties (Alt+Enter) módon.
Most nem ez kell, hanem a másik, amit a projekten állva,
F4-et nyomva lehet előszedni (ez a Property Window).
Tehát erről a jobb oldali ablakról beszél a readme.txt:
(bekereteztem a beállítani valókat):

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:

Windows hitelesítés esetén a bejelentkezett felhasználóról, az előző részben látott HttpContext.User


tulajdonságból nyerhetünk információkat. A User-ben ilyenkor egy WindowsPrincipal objektum
található. Ennek propertyjei pedig a Windows környezetre jellemző tulajdonságokkal rendelkeznek,
mint például a munkacsoportos vagy tartományi csoporttagságok.

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

9.3.3. OAuth, OpenID

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:

Mielőtt kipróbáljuk érdemes az alkalmazásunkat megjelenítő böngészővel kijelentkezni a Google


szolgáltatásaiból, ha be lennénk jelentkezve (gmail), hogy lássuk a folyamatot. A gombra kattintva a
böngésző átugrik a Google bejelentkezési szolgáltatására:

Az email cím és a jelszó megadása után még egy biztonsági


megerősítést kér:
9.3 A biztonság és az értelmes adatok - Felhasználó hitelesítés 1-259

A következő lépésben a vezérlés visszakerül a mi


alkalmazásunkhoz és a Google fiókunkat regisztrálhatjuk az
alkalmazásunkba. A User name a helyi rendszerünkben
megjelenő nevet jelenti és nem a Google email címet.

Ezzel be is jelentkezünk. A Hello, [felhasználó név] linkre kattintva az


account-kezelő oldalon találjuk magunkat.

Itt megtehetjük, hogy az előbb regisztrált felhasználóhoz még egy jelszót


rendelünk. Ez a helyi rendszerünkben fog tárolódni és nem érinti a
Google fiókunkat. Ezzel lehetőséget adunk a felhasználónak, hogy a
Google hitelesítésen kívül még a mi rendszerünk SimpleMemberShip
hitelesítését is használhassa.

Ha több hitelesítő szolgáltatóhoz is engedélyezzük a kapcsolódást az


AuthConfig.cs-ben, akkor azok alul a Google gomb mellett megjelennek.
Ekkor ezeket a szolgáltatókat is összerendelhetjük a helyi profillal.

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

A regisztrációs procedúrának ennél a pontjánál a szolgáltató egy visszahívási URL-re irányította a


böngészőt. A szolgáltató már hitelesítette a felhasználót az ő részéről, és elküldi a hitelesítési
csomagját. Ez a csomag a háttérben egy halom cookie-t jelent és szolgáltatónként változó tartalmú.
Emiatt a csomag egy része egy ExtraData nevű dictionary-be kerül. Az AccountController
ExternalLoginCallback actionjének az elején ez a csomag lekérdezésre kerül.

AuthenticationResult result = OAuthWebSecurity.VerifyAuthentication(Url.Action("ExternalLoginCallback",


new { ReturnUrl = returnUrl }));

A result-ban ekkor ki van töltve a result.ExtraData["email"] és a result.UserName. Naná, hogy


mindkettő az email címet tartalmazza. Ahhoz, hogy lekérdezzük a valós felhasználói nevet egy
leszármaztatott OpenID kliensben bővíteni kell, a hitelesítési kérést azzal, hogy milyen extra
információkra vagyunk még kíváncsiak.

public class GoogleWithFullNameClient : OpenIdClient


{
public const string FULLNAME_KEY = "Fullname";

public GoogleWithFullNameClient()
: base("Google", WellKnownProviders.Google) { }

protected override void OnBeforeSendingAuthenticationRequest(IAuthenticationRequest request)


{
var fetchRequest = new FetchRequest();
fetchRequest.Attributes.AddRequired(WellKnownAttributes.Name.First);
fetchRequest.Attributes.AddRequired(WellKnownAttributes.Name.Last);
request.AddExtension(fetchRequest);
}

protected override Dictionary<string, string> GetExtraData(IAuthenticationResponse response)


{
var fetchResponse = response.GetExtension<FetchResponse>();
if (fetchResponse == null) return null;

var result = new Dictionary<string, string> ();


var fullname = fetchResponse.GetAttributeValue(WellKnownAttributes.Name.First) + " " +
fetchResponse.GetAttributeValue(WellKnownAttributes.Name.Last);
result.Add(FULLNAME_KEY, fullname);
return result;
}
}

A művelet egy request és egy response párra tagozódik. Az OnBeforeSendingAuthenticationRequest-


ben összeállítjuk azokat az adatokat egy FetchRequest objektumban, amikre még szükségünk van.
Most a First és a Last name-re. Ezeket az adatokat egy URI séma szerint lehet megcímezni. Például a
first name URI-ja: http://axschema.org/namePerson/first. Ezeket egy konstans stringeket tartalmazó
WellKnownAttributes osztályon keresztül könnyen elérhetjük. Ilyen jól definiált adatok például a
születési dátum, telefonszámok, lakcím, munkahely és az Instant Messages azonosítók (pl. skype). Jó
azzel is tisztában lenni, hogy egy ilyen nyilvános hitelesítés szolgáltató simán kiadja ezeket az adatokat
az API-ján keresztül. A regisztrációs procedúrában erről az adatkiadásról szinte semmi sem tájékoztatja
a laikus felhasználót.

A szolgáltatótól érkezett válaszból a GetExtraData metódusban lehet kiszedegetni a kapott extra


információkat. A visszatérési értéke egy szótár, amit az AccountController.ExternalLoginCallback
actionben tudunk viszontlátni.

AuthenticationResult result = OAuthWebSecurity.VerifyAuthentication(Url.Action("ExternalLoginCallback",


new { ReturnUrl = returnUrl }));
...
string fullname = result.ExtraData[GoogleWithFullNameClient.FULLNAME_KEY];
9.4 A biztonság és az értelmes adatok - Kódolt azonosítók 1-261

// User is new, ask for their desired membership name


string loginData = OAuthWebSecurity.SerializeProviderUserId(result.Provider, result.ProviderUserId);
ViewBag.ProviderDisplayName = OAuthWebSecurity.GetOAuthClientData(result.Provider).DisplayName;
ViewBag.ReturnUrl = returnUrl;
return View("ExternalLoginConfirmation", new RegisterExternalLoginModel { UserName = fullname,
ExternalLoginData = loginData });

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.

9.4. Kódolt azonosítók

Manapság minden értékes adathalmazunkat egy számsorral azonosítunk. Taj-szám, bankszámlaszám,


user id, számla id, account id. Ha én hacker lennék, ilyen azonosítókra vadásznék, hiszen ez a kulcs egy
adat megszerzéséhez. Azonban akárhogy csűrjük csavarjuk, a kiküldött HTML formot vagy JSON
csomagot valamilyen azonosítóval kell ellátni, ami rendszerint kapcsolatot biztosít a háttérben egy
adattábla egy sorához (entitásazonosító). Ez az azonosító megjelenhet az URL-ben, mint route
paraméter (Id) vagy query string és majd erre az URL-re kerülhet elküldésre a form. Esetleg lehet egy
hidden mezőben vagy egy javascript változóban. Ezeknek az azonosítóknak a validálása nem a
hagyományos értelemben vett érvényesítés, hogy az ügyfél jól adta-e meg, hiszen a mi kódunkkal
generáltuk. Mivel az ügyfél nem is változtatja meg, ezért célszerű titkosítani. Már csak azért is, mert
elég veszélyes olyan kulcsot kiadni a kliensnek, ami esetleg egy entitásazonosító. Ez technikai adat,
miért jelenjen meg a felhasználói interfészen?

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

alkalmazásunk küldte ki még a POST-ot megelőző Get requestnél, a mi alkalmazásunk töltötte ki a


modellt. Ez csak egy illúzió, ne dőljünk be neki! A tábla sor egyedi azonosítója egy hidden field-ből jött,
amit arra ír át a ’felhasználó’, amire akar! És bizony nem is hibáztathatjuk a fejlesztőket, ha ezt az Id
kezeléstechnikát készpénznek veszik, mert hát a VS template ezt sugallja. Ne felejtsük azonban el, hogy
amit a template-k nyújtanak, azt leginkább azért teszik, hogy kezdő lökést adjanak a fejlesztéshez, és
a tanuláskor kezdetben is sikereket érjünk el. Ahogy készülünk az első éles alkalmazásunkkal, további
tényeket is meg kell értenünk.

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.

Azonosítók hidden mezőben. Az AntiForgery bővítése

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();
}

public ActionResult Index()


{
this.RouteData.Values.Add("hiddenid", 12);
return View();
}

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

találni a ”hiddenid” paraméternévhez értéket, ha odatesszük előtte. (Megjegyzésként: a RouteData


nem perzistens tároló két request között, nem olyan mint a Session, vagy a TempData)

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult AntiForgeryServed(int hiddenid)
{
return Content("Minden rendben "+hiddenid);
}

Az AdditionalDataProvider két metódusa lebonyolítja a RouteData kezelését. A GetAdditionalData


meghívásra kerül a View-ban levő @Html.AntiForgeryToken() miatt. Ha talál „hiddenid” bejegyzést (az
index actionben töltöttük bele), akkor a visszatérési értékként szolgáltatva bekerül a tokenbe.
Szerencsére ez az actionből való kilépés után hívódik meg. Ez volt a get request kiszolgálása. A válasz
post request feldolgozása során még az action hívása és a model binder aktivizálódása előtt meghívásra
kerül a ValidateAntiForgeryToken attribútum miatt a lenti ValidateAdditionalData, ahol az
additionalData paraméterből az értéket (”12”) megint csak a RouteData gyűjteményében tároljuk. Ezt
fogja a model binder megtalálni. Végül megjelenik a hiddenid paraméterben. Az egészben az a szép,
hogy a folyamat elején egy int értéket bocsájtottunk útjára és a végén szintén egy int értéket kaptunk
vissza.

public class HiddenAFDataProvider : System.Web.Helpers.IAntiForgeryAdditionalDataProvider


{
public string GetAdditionalData(HttpContextBase context)
{
var mvchandler = context.Handler as MvcHandler;
if (mvchandler == null ||
!mvchandler.RequestContext.RouteData.Values.ContainsKey("hiddenid")) return String.Empty;
return mvchandler.RequestContext.RouteData.Values["hiddenid"].ToString();
}

public bool ValidateAdditionalData(HttpContextBase context, string additionalData)


{
var mvchandler = context.Handler as MvcHandler;
if (mvchandler == null) return false;

mvchandler.RequestContext.RouteData.Values.Add("hiddenid", additionalData);
return true;
}
}

Az egyértelműség kedvéért a folyamat lépései:

1. Index action: tároljuk a 12-őt a RouteData-ban.


2. Index View: Html.AntiForgeryToken() -> GetAdditionalData meghívódik.
3. Eljut az oldal a böngészőbe. A submit gombnyomásra indul vissza a form.
4. Az MVC megtalálja a ValidateAntiForgeryToken attribútumot, emiatt hívása kerül a
ValidateAdditionalData és az additinalData->hiddenid visszakerül a RouteData-ba.
5. Az AntiForgeryServed actionnek szüksége van a hiddenid paraméterre. Indul a model
binder és ez a RouteDataValueProviderFactory segítségével megtalálja a RouteData-ban a
hiddenid-t.

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

Azonosítók speciális hidden mezőben.

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.

Az oda-vissza kódolónak van egy érdekessége, hogy a titkosítandó objektumot a JavaScriptSerializer-


rel sorosítja és alakítja vissza. Működik DateTime típussal is, mivel ebben az esetben nincs JS
felhasználás, a saját nyelvét meg megérti. A titkosítás kimenete egy base64 kódolású string lesz.

public static class StringEncoderHelper


{
static readonly byte[] SecurityKey = new byte[] {52,41,30,29,18,7,24,24,11,19,45,89,11,4,9,51};
static readonly byte[] InitVector = new byte[] {99,30,29,18,7,24,24,11,19,45,89,11,4,9,51,41};
static readonly RijndaelManaged Rijndael;
//.Net 4.5 private const string purpose = "hiddenfield demo v1";

static StringEncoderHelper()
{
Rijndael = new RijndaelManaged
{
Mode = CipherMode.CBC,
Padding = PaddingMode.PKCS7,
KeySize = 128, BlockSize = 128, Key = SecurityKey, IV = InitVector
};
}

public static string GetEncodedString(object toEncode)


{
string timestamp = string.Format("{0:X16}", DateTime.Now.Ticks);
string ser = timestamp + new JavaScriptSerializer().Serialize(toEncode);

byte[] buffer = Encoding.UTF8.GetBytes(ser);


//.Net 4.5 byte[] encoded = System.Web.Security.MachineKey.Protect(buffer, purpose);
byte[] encoded = Rijndael.CreateEncryptor().TransformFinalBlock(buffer, 0, buffer.Length);
return System.Web.HttpServerUtility.UrlTokenEncode(encoded);
}

public static object GetDecoded(string encodedString)


{
byte[] encoded = System.Web.HttpServerUtility.UrlTokenDecode(encodedString);
//.Net 4.5 byte[] decoded = System.Web.Security.MachineKey.UnProtect(encoded, purpose);
byte[] decoded = Rijndael.CreateDecryptor().TransformFinalBlock(encoded, 0, encoded.Length);
string ser = Encoding.UTF8.GetString(decoded);

string timestamptxt = ser.Substring(0, 16);


long timestamp;
if (long.TryParse(timestamptxt,NumberStyles.HexNumber,null, out timestamp) )
{
9.4 A biztonság és az értelmes adatok - Kódolt azonosítók 1-265

if (new DateTime(timestamp).AddMinutes(20) > DateTime.Now) //20 perc lejárati idő


return new JavaScriptSerializer().DeserializeObject(ser.Substring(16));
throw new SecurityException("A token ideje lejárt");
}
throw new SecurityException("Hibás időbélyeg");
}
}

A .Net 4.5 framework alatt a RijandaelManaged titkosítás helyett használhatjuk a beépített


MachineKey által szolgáltatott megoldást is. Ezt 4.0 alatt nem tudtam felhasználni, mert csak az Encode
és Decode metódusai léteznek a történelemnek ebben a szakaszában, és ezek számunkra most
feleslegesen hosszú hexa stringekkel dolgoznak. A titkosított kód meg van „bolondítva” egy
időbélyeggel, ami 20 perces lejárati határidőt határoz meg. Az időbélyeg miatt a hidden mezőbe kerülő
kód minden request esetében más lesz, hasonlóan az AntiForgery tokenhez. (Ettől lesz „paprikás” a
mókus)

A HttpServerUtility.UrlTokenEncode/UrlTokenDecode használata azért előnyös, mert ha más szöveges


kódolási módszert használnánk, akkor a HTML-be/URL-be nem helyezhető, illegális karaktereket (pl. +
/ & =) nekünk kellene lecserélnünk a kódolás-dekódolás során.

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 class SecurityHtmlExtension


{
public const string SecurityHiddenFieldNamePrefix = "hobj-";

public static MvcHtmlString EncodedHidden(this HtmlHelper htmlHelper, string name, object value)
{
if (value == null) return MvcHtmlString.Empty;

var encoded = StringEncoderHelper.GetEncodedString(value);


return InputExtensions.Hidden(htmlHelper, SecurityHiddenFieldNamePrefix + name, encoded);
}
}

A Html helperben azért, hogy majd a ValueProviderFactory-ban könnyen azonosítani tudjuk a


titkosított hidden mezőket, a nevük elé bekerül egy szabadon választott „hobj-” prefix (magic string).
Paraméterként vár egy property nevet és a tárolandó objektumot. Nem is kell mást tenni, mint a név
elé tolni a prefixet, majd ennek eredményét és a value értéket továbbdobni az InputExtensions statikus
helper osztályban megvalósított Hidden metódusba. A normál Html.Hidden is ezt a metódust
használja. Az hogy felhasználjuk a háttér infrastruktúrát, egy nagyon kényelmes módja a Html helper
metódusok készítésének. Egyébként sem nehéz.

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:

public ActionResult Index()


{
this.RouteData.Values.Add("hiddenid", 12);
return View(SecurityModel.CreteNewModel());
}

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

public class SecurityModel


{
public int HId { get; set; }
public Guid HGuid { get; set; }
public string FullName { get; set; }

private static int tid;


public static SecurityModel CreteNewModel()
{
return new SecurityModel()
{
HId = ++tid,
HGuid = Guid.NewGuid(),
FullName = "Paprikás Mókus " + tid
};
}

public override string ToString()


{
return string.Format("Hid: {0} Guid:{1} Név: {2}", HId, HGuid, FullName);
}
}

public class SecurityStorageModel


{
public SecurityModel SecurityModelInternal { get; set; }
public int Sid { get; set; }
}

A következő a ValueProviderFactory megvalósítása. Ennek az a feladata, hogy a GetValueProvider


metódussal biztosítsunk egy IValueProvider megvalósítást, ami majd szolgáltatja a konkrét adatokat a
propertykhez a nevük (és bejárási path-uk) alapján. Ebben az esetben az MVC beépített
DictionaryValueProvider-e lesz, amit erre használunk, feltöltve a propertynév-érték párokkal. Itt tudjuk
felhasználni azt, hogy előzőleg előtagot adtunk a titkosított hidden mezők neveinek, mert könnyen
összeszedhetjük a Request objektum Form kollekciójából.

public class EncryptedValueProviderFactory : EncryptedValueProviderFactoryBase


{
public override IValueProvider GetValueProvider(ControllerContext controllerContext)
{
var provided = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
try
{
//Megkeressük a hobj- karakterekkel kezdodő input elemeket
foreach (string inputname in
controllerContext.HttpContext.Request.Form.Keys.Cast<string>()
.Where(inputname =>
inputname.StartsWith(SecurityHtmlExtension.SecurityHiddenFieldNamePrefix)))
{
//levágjuk az input név elejéről a hobj- -et
string rootinputname = inputname.Substring(
SecurityHtmlExtension.SecurityHiddenFieldNamePrefix.Length);
//Visszaalakítjuk a szöveges adatot objektummá
var decoded = StringEncoderHelper.GetDecoded(
controllerContext.HttpContext.Request.Form[inputname]);
//Rekurzívan bejárva az objektumot feltöltjük a szótárat azokkal
//a property path-okkal, amikhez tudunk értéket szolgáltatni
AddToProvidedList(provided, rootinputname, decoded);
}
if (provided.Count == 0) return null;
return new DictionaryValueProvider<object>(provided,
System.Globalization.CultureInfo.CurrentCulture);
}
catch (Exception ex)
{
throw new InvalidOperationException("Hibás adat", ex);
}
}
}

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.

public abstract class EncryptedValueProviderFactoryBase : ValueProviderFactory


{
//Rekurzív
protected static void AddToProvidedList(Dictionary<string, object> provided,
string prefix, object value)
{
var d = value as IDictionary<string, object>;
if (d != null)
{
foreach (KeyValuePair<string, object> entry in d)
AddToProvidedList(provided, MakePropertyKey(prefix, entry.Key), entry.Value);
return;
}

var l = value as IList;


if (l != null)
{
for (int i = 0; i < l.Count; i++)
AddToProvidedList(provided, MakeArrayKey(prefix, i), l[i]);
return;
}
provided.Add(prefix, value);
}

private static string MakeArrayKey(string prefix, int index)


{
return prefix + "[" + index.ToString(System.Globalization.CultureInfo.InvariantCulture) + "]";
}

private static string MakePropertyKey(string prefix, string propertyName)


{
return (String.IsNullOrEmpty(prefix)) ? propertyName : prefix + "." + propertyName;
}
}

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.

private void Application_Start()


{
//… többi MVC regisztáció …
ValueProviderFactories.Factories.Insert(0, new Controllers.Securities
.EncryptedValueProviderFactory());
}

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

<h3>Saját kódolt hidden html extension</h3>


@using (Html.BeginForm("EncodedHidden", "Security"))
{
@Html.EncodedHidden("Hid", Model.HId)
@Html.EncodedHidden("HGuid", Model.HGuid)

@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.

<h3>Saját, kódolt hidden html extension</h3>


<form action="/Security/EncodedHidden" method="post">
<input id="hobj-Hid" name="hobj-Hid" type="hidden" value="nUrEIuIHwNCRcrPtQMzNBw" />
<input id="hobj-HGuid" name="hobj-HGuid" type="hidden"
value="0PMuFLW+7fdOjhY+9PbLJytHKa7zvumP7v8wtZMuoL1RYUYgxw47PjGUlrl4Ys/S" />
<input id="FullName" name=" FullName " type="text" value="Paprikás Mókus 1" />

<input type="submit" value=" Ment " />


</form>

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:

<h3>Saját kódolt hidden html extension objektummal</h3>


@using (Html.BeginForm("EncodedInternalHidden", "Security"))
{
@Html.EncodedHidden("SecurityModelInternal", Model)
@Html.EncodedHidden("Sid", 9999)

@Html.Display("FullName")<br />
@Html.Display("HId")<br />
<input type="submit" value=" Ment " />
}

A „SecurityModelInternal” és a „Sid” a SecurityStorageModel propertyjének a nevei. Alatta ott vannak


a modell két propertyjének a megjelenítői, csak azért, hogy lássuk mi érkezett a böngészőbe és minek
kéne megjelennie a post után.

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

Mikor a DictionaryValueProvider-be kerülnek, a propertyk nevei kiegészülnek a rootinputname-ben


ideiglenesen tárolt gyökér property névvel. Ezek lesznek a dictionary kulcsok a beágyazott
SecurityModelInternal property nevekhez. Végül azt látja majd a model binder a providerből, amire
szüksége van:
9.4 A biztonság és az értelmes adatok - Kódolt azonosítók 1-270

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.

Az URL titkosítását bonyolítja, hogy az entitásazonosítónak és a titkosított kódnak egymással


egyértelműen megfeleltethetőnek, ráadásul még permanensnek is kell lennie. Ez a kitétel nyilvános
oldalaknál fontos. Ilyen esetben nem tudjuk alkalmazni a „minden requestre új kódot generálunk”
metodikát, amit az AntiForgery rendszer is csinál. Itt egy kompromisszumra lesz szükség, attól függően,
hogy mi a fontosabb. Az, hogy a SEO szempontoknak megfelelően azonos maradjon az azonosító44,
vagy az azonosító megváltozhat, mert a biztonság fontosabb. Ez utóbbi megközelítés a felhasználói
hitelesítés után elérhető belső oldalak esetén szokott előkerülni, például egy bank online felületén.
Ekkor az entitás azonosítóba a felhasználó azonosítójából képzett hash és egy rövid lejáratú időbélyeg
is belekerülhet. Ezzel kapunk egy nem hordozható, (nagyjából) egyszer használható URL-t.

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.

public class EncryptedRouteAttribute : FilterAttribute, IAuthorizationFilter


{
public void OnAuthorization(AuthorizationContext filterContext)
{
var routedata = filterContext.RouteData;
var ritem = routedata.Values["id"];
if (ritem == null) throw new InvalidOperationException("Hiányzó Id");
routedata.Values.Remove("id"); //Lecseréljük

var provided = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);


var decoded = StringEncoderHelper.GetDecoded((string)ritem);
string rootinputname = string.Empty;
if (!(decoded is IEnumerable))
rootinputname = "id";
EncryptedValueProviderFactoryBase.AddToProvidedList(provided, rootinputname, decoded);
foreach (KeyValuePair<string, object> keyValuePair in provided)
routedata.Values.Add(keyValuePair.Key, keyValuePair.Value);
}
}

A View és az actionök példakódjai maradtak csak hátra:

<h3>Elkódolt Id az URL-ben</h3>
@Html.EncodedActionLink("Új oldal, ahova az Id elkódolva érkezik",
"EncodedUrl","Security", Model.HId)

<h3>Elkódolt Id és további query string az URL-ben</h3>


@Html.EncodedActionLink("Új oldal, ahova az Id és a query string elkódolva érkezik",
"EncodedUrlQuery","Security",
new Dictionary<string, object>()
{{"id", Model.HId}, {"mokusnev", Model.FullName}})

A filter attribútummal tudjuk beindítani az URL/RouteData dekódolását:

[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

 A RouteData tartalmának bővítésével tudtunk olyan értéket szimulálni a model binder


számára, ami meg sem jelent a postot megelőző get request folyamán. Se az URL-ben, se a
formon. Ezt megtehetjük a filterekben vagy legkésőbb a ValueProviderek-ben.
 Láttunk egy módot, hogy lehet átalakítani egy objektumot, sorosítással, hogy az a HTML-ben
és URL-ben is tárolható legyen.
 Megnéztük egy ValueProviderFactory megvalósításon és felhasználáson keresztül, hogy mit
kell szolgáltatnia ahhoz, hogy az action számára emészthető típusos metódus paraméterek
keletkezzenek.
 Láttuk hogyan lehet saját Html helper bővítést készíteni, ha csak apró változásra van
szükségünk a gyári megvalósításhoz képest.
 A JavaScriptSerializer működésének másik dekódoló oldalát is megismertük.

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.

if (long.TryParse(timestamptxt,NumberStyles.HexNumber,null, out timestamp) )


{
if (new DateTime(timestamp).AddMinutes(20) > DateTime.Now) //20 perc lejárati idő
return new JavaScriptSerializer().DeserializeObject(ser.Substring(16));
throw new SecurityException("A token ideje lejárt");
}
throw new SecurityException("Hibás időbélyeg");

É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)

 Támadások kivédése. Az adatok veszélyesé válhatnak, ha nincs „fegyver- és fémdetektor” a


bejövő adatok kapujában. Ma már közismert, hogy az SQL és a javascript injektálást ki kell
védeni. Az MVC az utóbbira, az ADO.NET pedig az előbbire ad védelmet, ha élünk vele.
Általában mikor validációról beszélnek, nekem az első asszociációm, hogy szöveges vagy
dátum beviteli mezőbe érvénytelen adatot visznek be, pedig sokkal tágabb dologról van szó.
Hogy egy „átgondolandó” validációs példát is említsek: ott van a mindenki által használt jelszó
validáció. Ha bejelentkezésnél rossz jelszót írunk be, a rendszer ellenőrzi, ha nem jó
visszadobja. Az igazi kérdés nem is az, hogy ezt validálni kell, hanem a lényeg, hogy hányszor
lehet validálni egy időszak alatt. Itt megjelenik az időtényező és a próbálkozások száma, mint
validálási faktor.
9.5 A biztonság és az értelmes adatok - Validálás 1-274

Egy képzeletbeli, webalkalmazásnál legalább 5 szintje van a validációnak.

 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.

A validálási célok két kategóriára lebonthatóak.

1. Az entitás (modell) az értékeivel önmagában érvényes.

2. Az entitás más entitásokkal, vagy kontextussal (idő, ismétlődés) összefüggésben érvényes.

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.

9.5.1. A szerver oldalon

Kezdjük a szerver oldalon történő vizsgálódással, kukacoskodással. Emlékeztetőül, a validáció a model


binder működésével kapcsolatban lép működésbe. Két esetben indul el: akkor, ha az actionünk
paramétert vár és akkor, ha magunk indítjuk az UpdateModel, TryUpdateModel, ValidateModel vagy
a TryValidateModel metódusokkal. A request adatokat a modell property- vagy paraméternevekkel
párosítja, és az értéket beleírja a propertykbe (vagy a paraméterekbe). A beleírás mellett megvizsgálja
a propertyhez tartozó ModelMetadata tulajdonságait 47 is és validációs interfészei alapján indítja a
modellszintű validációt is. A hibákat hibaüzenetestül, a kontroller ModelState propertyjében tárolja.
Lehetőségünk van a ModelState.IsValid értékét vizsgálva tudomást szerezni arról, hogy volt-e

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.

public class ValidationMaxModel


{
[HiddenInput]
public int Id { get; set; }

[Display(Name = "FullNameLabel", ResourceType = typeof(Resources.UILabels))]


[Required(ErrorMessage = "A név megadása kötelező (1)!")]
public string FullName { get; set; }

[Display(Name = "Vásárló címe")]


[DataType(DataType.MultilineText)]
public string Address { get; set; }

[Display(Name = "Vásárló email")]


[DataType(DataType.EmailAddress)]
public string Email { get; set; }

[Display(Name = "Utolsó vásárlás")]


[DataType(DataType.Date)]
[Required]
public DateTime LastPurchaseDate { get; set; }

[Required]
public int RequiredInt { get; set; }

[Required]
public bool RequiredBool { get; set; }

public static ValidationMaxModel GetModell(int id)


{ //A szokásos memória alapú tároló }
}

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:

@using (Html.BeginForm(null, "Validations", new { id = Model.Id }, FormMethod.Post))


{
@Html.LabelFor(m => m.FullName) @Html.TextBoxFor(m => m.FullName)<br />
@Html.ValidationMessageFor(m=>m.FullName)

<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" />
}

Emiatt valami probléma lehet majd, ha validálni akarjuk a [Required] propertyket.


9.5 A biztonság és az értelmes adatok - Validálás 1-276

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");

isValid = this.ModelState.IsValid; // = false

this.ModelState.Clear();

var model = ValidationMaxModel.GetModell(id.Value);


inputModel.LastPurchaseDate = model.LastPurchaseDate;

//Exception-t generáló változat: this.ValidateModel(inputModel);


if (this.TryValidateModel(inputModel))
{
//Igen mostmár valid.
isValid = this.ModelState.IsValid; // = true
}

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, mint property név a kontrolleren.


- a ModelState, mint a belső listaelemek típusa.

A jobb oldali képen a 0. Values


elem van kinyitva, ami az "Id"
értékéhez tartozik. Látszik a
nyers "1" adat, ami a formról
érkezett.

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.

Most tegyünk valami komolyabb validációt is a LastPurchaseDate propertyre:

[Range(typeof(DateTime), "2010.01.01", "9999.12.31")]


[Required]
public DateTime LastPurchaseDate { get; set; }

Ú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

A példakódban a további részek még hasznosak lesznek. A ModelState.Clear()-el tisztára mossuk a


validációs állapotot. A hívása után a ModelState.IsValid = true lesz.

A következő példában az inputmodel-be a háttér adatforrásból érkező modellből kimásolva, feltöltjük


a dátum értékét. Ezután megismételhetjük a validációt. Erre két módszer is van, ami nem változtat a
modell adatain (most az inputmodel-en):

- A ValidateModel exceptiont vált ki, ha a validáció nem sikerül.


- A TryValidateModel csak finoman egy false-al jelzi ugyanezt.

this.ModelState.Clear();

var model = ValidationMaxModel.GetModell(id.Value);


inputModel.LastPurchaseDate = model.LastPurchaseDate;

//Exception-t generáló változat:


//this.ValidateModel(inputModel);

//If-es szerkezet:
if (this.TryValidateModel(inputModel))
{
//Igen mostmár valid.
isValid = this.ModelState.IsValid; // = true
}

Ha még emlékszünk, megvan ezeknek a metódusoknak az a verziója is - UpdateModel és


TryUpdateModel néven - amik a modellt egyből fel is töltik.

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.

ModelState.AddModelError("FullName", "Manuális hibaüzenet a Felhasználó névhez");

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.

ModelState.AddModelError("", "Modell szintű hibaüzenet");

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

@Html.ValidationSummary(true, "Modell szintű validációs problémák")

Egyedi validátorok

A validációs attribútumoknál megismertük a CustomValidation attribútumot, ami lehetőséget ad arra,


hogy egy kapcsolódó osztály metódusában, vagy akár a modell egy saját metódusában validációs
logikát állítsunk össze. Hasznos dolog, ha nem a modellben implementáljuk a validációs metódust,
mert akkor hordozhatóvá válik és más modellhez is használhatjuk. A CustomValidation egy korlátja,
hogy csak egy validációs eredményt tudunk üzenni vele, egy nagy összesített hibaüzenetet. Modellen
használva, ami több property validációját jelenhetné, nem túl praktikus. Rejteget még az MVC néhány
további validációs lehetőséget is ezen felül. Például olyat, ami nyitva hagyja az ajtót a kliens oldali
validáció felé. Az egyedi validációs technikák továbbfejlesztésében, több irányban is el lehet indulni.

 CustomValidation, amit már ismerünk.


 Új attribútumot készítünk a ValidationAttribute absztrakt osztály alapján.
 Meglevő validációs attribútumokból származtatva, felülbíráljuk a működését.
 A modellünkkel megvalósítjuk az IValidatableObject interfészt.

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

Ez az interfész biztosítja a lehetőséget az önvalidáló modellek definíciójára. A következő példákban az


LastPurchaseDate nevű propertyvel fogunk kísérletezni. Most szükséges, hogy ne legyen rajta
semmilyen validációs attribútum. (A kiértékelési sorrend miatt azok előbb érvényesülnének). Tehát
egy ilyennel induljuk el:

[Display(Name = "Utolsó vásárlás")]


public DateTime LastPurchaseDate { get; set; }

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 következőleg felhasznált modelleknek a sajátossága, hogy a ValidationMaxModel-ből származnak,


saját propertyjük nincs. Ez egy jó szemléltetés lesz a leszármazott modellek használatára is.

Az első variációban már látható is, hogy az IValidatableObject egymetódusos interfészdefiníciója


szerint csak a Validate metódust kell megvalósítani:

public class ValidationMaxIVOModel : ValidationMaxModel, IValidatableObject


{
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
//validációs hibák gyűjteménye:
var results = new List<ValidationResult>();

if (this.LastPurchaseDate > DateTime.Now ||


this.LastPurchaseDate < DateTime.Today.AddYears(-2))
{
//Új validációs hiba:
results.Add(new ValidationResult("Az Utolsó vásárlás dátuma nem lehet a jövőben vagy
mielőtt a bolt megnyílt!",
new[] { "LastPurchaseDate","FullName" }));
}
return results;
}

public static new ValidationMaxIVOModel GetModell(int id)


{
return new ValidationMaxIVOModel()
{
Id = id,
FullName = "Tanuló " + id,
Address = string.Format("Budapest {0}. kerület", id + 1),
Email = "proba@proba.hu",
LastPurchaseDate = DateTime.Now.AddDays(-2 * id)
};
}
}

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.

A második modellvariációban a Data annotation rendszer statikus Validator osztályának a


[Try]ValidateValue metódusával validálunk. Ennek az érdekessége, hogy utólag tudunk validációs
attribútumokat „ráhúzni” a property értékére (a propertyre magára nem). Mintha az osztályban
definiáltuk volna. A lenti példában a Range attribútum belső validációját idézzük meg.

public class ValidationMaxIVOModel : ValidationMaxModel, IValidatableObject


{
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
var results = new List<ValidationResult>();
if(!Validator.TryValidateValue(this.LastPurchaseDate, validationContext, results, new[]
{
new RangeAttribute(typeof(DateTime),
DateTime.Today.AddYears(-1).ToString("d"),
DateTime.Today.AddYears(1).ToString("d"))
}))
{
var badresults = new List<ValidationResult>();
foreach(var validationResult in results)
badresults.Add(
new ValidationResult(validationResult.ErrorMessage, new[] { "LastPurchaseDate" }));

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 { . . . }

public class ValidationMaxIVOModelMetaData


{
[Range(typeof(DateTime), "2012.01.01", "2013.12.31")]
public DateTime LastPurchaseDate { get; set; }
}

Tapasztalatszerzés célzatú rejtvény következik. Mi történik akkor, ha az előbbi


ValidationMaxIVOModelMetaData osztály mellett az alaposztályon is definiálom a Range
attribútumot? Melyik lesz aktív, ha egy 2011-es dátumot szeretnék érvényesíteni?
9.5 A biztonság és az értelmes adatok - Validálás 1-282

Az alaposztály propertyjén nézzen ki így a definíció:

[Range(typeof(DateTime), "2010.01.01", "9999.12.31")]


public DateTime LastPurchaseDate { get; set; }

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.

Új Validation attribútum létrehozásakor mindössze az IsValid metódust kell felülbírálni. Ez a metódus


megkapja a validálandó értéket és a ValidationContext-et, pont úgy, mint az előző interfész alapú
validációnál. A példakód által megvalósított attribútum használata a modellen a mai naphoz képest,
relatív dátumot érvényesít:

[Display(Name = "Utazási nap")]


[RelativeDateValidator(RelativeDateValidatorAttribute.RelativeDate.ElozoHonap)]
public DateTime TravelDate { get; set; }

[AttributeUsage(AttributeTargets.Property)]
public class RelativeDateValidatorAttribute : ValidationAttribute
{
public enum RelativeDate
{
ElozoHonap, Ma, KovetkezoHonap
}

private readonly RequiredAttribute innerRequired = new RequiredAttribute();


protected readonly RelativeDate rdate;

public RelativeDateValidatorAttribute(RelativeDate relDate)


{
this.rdate = relDate;
}

protected override ValidationResult IsValid(object value, ValidationContext validationContext)


{
if (value == null || !innerRequired.IsValid(value))
return new ValidationResult("A dátum kitöltendő!");
DateTime datum = (DateTime)value;
switch (this.rdate)
{
case RelativeDate.ElozoHonap:
if (StartOfMonth(datum) != MonthStart(DateTime.Today, -1))
return new ValidationResult("A dátum csak múlt hónapi lehet!");
break;
case RelativeDate.Ma:
if (datum.Date != DateTime.Today)
return new ValidationResult("A dátum csak mai nap lehet!");
break;
case RelativeDate.KovetkezoHonap:
if (StartOfMonth(datum) != MonthStart(DateTime.Today, +1))
return new ValidationResult("A dátum csak a következő hónapi lehet!");
break;
default:
throw new ArgumentOutOfRangeException();
}
return ValidationResult.Success;
}

private static DateTime StartOfMonth(DateTime d)


{
return d.AddDays(-d.Day + 1);
}

private static DateTime MonthStart(DateTime d, int monthRel)


{
return StartOfMonth(d.AddMonths(monthRel));
}
}

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:

private readonly RequiredAttribute innerRequired = new RequiredAttribute();


..
if (value == null || !innerRequired.IsValid(value))

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.

Túl a Data Annotation validátorokon

Az eddig használt validációs megoldások a System.ComponentModel.DataAnnotations névtérben


definiált lehetőségeken alapultak. Azt mondhatnám, hogy ezzel az esetek legnagyobb részét le is lehet
fedni. Azonban van még néhány csemege az MVC tarsolyában, bár egy kicsit már avasak már.
Mindenesetre érdemes belekukkantani, mert itt megint egy bővítési lehetőséget találhatunk, már ha
saját validációs rendszert szeretnénk építeni. Az alábbi kód a validációs providerek listáját hordozza az
MVC keretrendszerben:

public static class ModelValidatorProviders


{
private static readonly ModelValidatorProviderCollection _providers =
new ModelValidatorProviderCollection()
{
new DataAnnotationsModelValidatorProvider(),
new DataErrorInfoModelValidatorProvider(),
new ClientDataTypeModelValidatorProvider()
};

public static ModelValidatorProviderCollection Providers


{
get { return _providers; }
}
}

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.

A DataErrorInfoModelValidatorProvider az IDataErrorInfo interfészt megvalósító


modellosztályokhoz készít validátorokat. Az interfész definíciója:

public interface IDataErrorInfo


{
string this[string columnName] { get; }

string Error { get; }


}

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.

A következő a sorban a ClientDataTypeModelValidatorProvider, ami kilóg a szerver oldali validátorok


sorából, mivel kliens validációt készít, viszont ott szerepel provider listában, így kézenfekvő itt említeni.
A működése egyszerű: a nem nullázható numerikus típusok számára (byte,int,long, stb.) "number"
jQuery validation plugin funkciót/validációt határoz meg. A DateTime típusú propertyk számára pedig
"date" jQuery validation funkciót. Hogy ezek pontosan mik és hogyan kell használni, a következő
fejezetben meglátjuk. A működés eredménye, hogy a nem nullázható számokhoz tartozó HTML beviteli
mezőkbe csak számot, és a DateTime típusú propertykhez tartozó mezőkbe csak dátumot lehet
megadni.

A "Vásárlások összértéke" a TotalSum decimális típusú


propertyn nem volt validációs attribútum, mégis megy a
validáció.

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 …

9.5.2. A kliens oldalon

Az MVC keretrendszer készen adja a lehetőséget arra, hogy a böngészőben elvégezhessük az


elővalidáció48jelentős részét. Ennek az alapját a jquery.validate és a jquery.validate.unobtrusive jQuery
pluginek biztosítják. Az MVC 4 / .Net 4.5 óta (csokorba szedve) meg tudjuk tenni azt, hogy a szükséges
JS könyvtárak egyszerre töltődjenek le a böngésző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.

Sajnos ez az állóképen nem látszik.

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 boolean paraméter el is hagyható a bekapcsoláshoz, mivel a true az alapértelmezett, a false-al pedig


kikapcsolható a funkcionalitás. Ugyan ez vonatkozik az unobtrusive lehetőségek engedélyezésére vagy
tiltására is.

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.

ValidationAttribute kliens oldali ellenőrzéssel

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.

public class RelativeDateClientValidationRule : ModelClientValidationRule


{
public RelativeDateClientValidationRule(
RelativeDateValidatorAttribute.RelativeDate relativeDate)
{
switch (relativeDate)
{
case RelativeDateValidatorAttribute.RelativeDate.ElozoHonap:
ErrorMessage = "A dátum csak múlt hónapi lehet (kliens)!";
break;
case RelativeDateValidatorAttribute.RelativeDate.Ma:
ErrorMessage = "A dátum csak mai nap lehet (kliens)!";
break;
case RelativeDateValidatorAttribute.RelativeDate.KovetkezoHonap:
ErrorMessage = "A dátum csak a következő hónapi lehet (kliens)!";
break;
default:
throw new ArgumentOutOfRangeException("relativeDate");
}

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:

data-[ValidationType]-[Parameter Key] = ”Parameter Value”.

Emellett majd megjelenik az ErrorMessage hibaüzenetet hordozó attribútum is:

data-[ValidationType] = ”ErrorMessage” formában.

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

Ha megnézzünk egy „gyári” Required validációs attribútum eredményét ugyanezt látjuk:

<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:

public class ModelClientValidationRequiredRule : ModelClientValidationRule


{
public ModelClientValidationRequiredRule(string errorMessage)
{
ErrorMessage = errorMessage;
ValidationType = "required";
}
}
9.5 A biztonság és az értelmes adatok - Validálás 1-289

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)

A propertyhez a megfelelő a Rule-t a Validation[For] Html helperek kérik le az MVC infrastruktúrájából,


ezzel most nincs is dolgunk, mert a GetClientValidationRules már beinjektálta oda a beállított
példányt. Ha most futtatjuk a kódot még mindig működni fog a szerver oldali validáció, csak még éppen
nem validál a böngészőben, mert a JS kóddal még adós vagyok.

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.

Próbáljuk meg így:

@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 adapter egysoros regisztrációja az adapterek listájához adja a ”daterelative” nevezetűt és


meghatározza a paraméter nevét is (reldate). A nevek a RelativeDateClientValidationRule osztályban
meghatározott ValidationType értékével és az egy darab paraméterének a nevével egyezik meg. Ezek
adják a kapcsolatot a C# rule osztály, a HTML data-* attribútumok és a JS validátor metódusa között.
A validator.addMethod szintén a ValidationType-ban tárolt névhez rendel validátor funkciót. Ennek a
funkciónak a visszatérési értékével jelezhetjük, hogy a validáció sikeres (true), vagy sikertelen (false).
A fenti kód mindig sikeres lesz, mert még nem teljes.

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:

 addSingleVal – Egy paraméterrel dolgozó validátor funkciót regisztrál. A paramétert nevesíteni


kell. Ezzel a szerverről érkezett értékkel tudjuk összehasonlítani, validálni az input mezőbe
gépelt adatot. De erre sincs megkötve a kezünk. A szerverről érkezhet egy input mező Id-je is
és akkor meg lehet csinálni azt is, hogy a validálandó mezőt az Id-vel azonosított mező
értékével vetjük össze.
 addBool – Paraméter nélküli validáció. Akkor jó, ha a validálandó input mezőnek valamilyen
általános szabálynak kell megfelelnie. Érvényes email cím, bankkártya szám, URL, helyes
dátumformátum.
 addMinMax – Kétparaméteres validáció, amihez két validátort is kell rendelni. Ezzel lehet
validálni értéktartományokat, ha mindkét paramétert megadjuk. De lehet csak alsó vagy csak
felső értéket validálni, ha csak az egyik paramétert adjuk meg. Értéktartomány esetén egymás
után kerül meghívásra ,mindkét validációs funkció. Ezt használja ki a Range és a StringLength
validációs .Net attribútum is.
 add – Ez az alapfunkció, amihez több paramétert is rendelhetünk. Ezt hívja az előző
addSingleVal, addBool és az addMinMax is. Ezt használja közvetlenül a helyes jelszót
érvényesítő validátor is, amivel a jelszó minimális hosszát ellenőrzi és, hogy tartalmaz-e
számokat is és/vagy megfelel-e egy reguláris kifejezésnek.

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!

$.validator.addMethod("daterelative", function (value, element, relativeDate) {


return true;
});
A "relativeDate" mint paraméternév nem kötött, akármilyen nevet adhatunk neki.
9.5 A biztonság és az értelmes adatok - Validálás 1-291

Végre következzen a validációt elvégző teljesen kifejtett funkció:

<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;
});

function GetRelativeMonthOnly(dateObject, offsetOfMonth) {


return new Date(dateObject.getFullYear(), dateObject.getMonth() + offsetOfMonth, 1);
}
</script>

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

Működés közben, mikor hibás dátumot írok be, már jön is a


hibaüzenet. Ott a (kliens) szöveg a végén, tehát ez a kliens oldali
validátorból származott.

A nagyszerű az egészben, hogy ez a validáció nem kizárólagos, a


többi validátor is aktívan üzemel és érvénytelen dátum esetén is
jelez:

Sőt, ha kikapcsolom a böngészőben a javascript futtatását49, attól


még a szerver oldali validáció működőképes marad.
Természetesen, most meg kellett nyomni az Elküldés gombot,
hogy megérkezzen az üzenet. Nincs ott a "(kliens)" szöveg.

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:

jQuery.validator.setDefaults({ onkeyup: false });

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

RemoteAttribute, kompozit validáció

A cél eléréséhez mindössze két dolgot kell tenni:

- kidekorálni a vizsgálandó propertyt egy megfelelően beállított Remote attribútummal


- készíteni kell egy actiont, ami elvégzi a validációt.

A modell, már megint buddy class-al, ha még nem lenne unalmas:

[MetadataType(typeof(ValidationMaxRemoteModelMetaData))]
public class ValidationMaxRemoteModel : ValidationMaxModel
{
public static bool IsNameReserved(string newname)
{
return datalist.Any(d=>d.Value.FullName == newname);
}

public static new ValidationMaxRemoteModel GetModell(int id) { . . .}

private static Dictionary<int, ValidationMaxRemoteModel> datalist;


}

public class ValidationMaxRemoteModelMetaData


{
[Remote("RemoteNameValidator", "Validations",
ErrorMessage = "Ez már foglalt, próbálj másikat (attribute message)",
HttpMethod = "Post")]
public string FullName { get; set; }
}

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ő:

 ErrorMessage, ErrorMessageResourceName, ErrorMessageResourceType – a szokásos


hibaüzenet meghatározási módok. Közvetlenül, vagy resource fájlon keresztül.
 HttpMethod – Lehet Get, vagy Post az action hívási metodika.
 AdditionalFields – További mezőket lehet bevonni a validációba. Ezeknek az értékei is
megérkeznek az actionbe.

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.

Az Remote attribútum AdditionalFields paraméterében megadott mező vagy vesszővel elválasztott


mezők is megérkeznek az actionbe, amit fel lehet használni a validálás pontosítására.

,AdditionalFields = "Address,Id")

Action szignatúra:

public JsonResult RemoteNameValidator(string FullName, string Address, int id)

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.

jQuery.validator.setDefaults({ onkeyup: false });

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:

public override bool IsValid(object value)


{
return true;
}

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:

protected virtual ValidationResult IsValid(object value, ValidationContext validationContext)


{ }

Ebből a metódusból és az actionből pedig meghívásra kerülhet a közös validációs kódblokk


(Validation). Ezt a kódblokkot lehet implementálni statikusan is:

A MyRemote attribútum össze van kapcsolva a controller, action paramétereivel a ValidationAction-


nel, ez végezteti majd el a kliens oldali validációt a MyRemoteAttribute.Validation metódussal.
9.5 A biztonság és az értelmes adatok - Validálás 1-296

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:

Extending the MVC3 RemoteAttribute to validate server-side when JavaScript is disabled

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

10. Reakcióképesség, gyorsítás, minimalizálás.

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ó.

Az gondolom teljesen világos, hogy a hálózati forgalommal, a processzorral, memóriával egyszóval az


erőforrásokkal takarékoskodni kell. Ebből a takarékoskodási, optimalizációs programból mindenki
kiveszi a részét. A szerver(ek) hardverkiépítettségének gyakorlatias (€) határa van, de nem árt
teletömni memóriával. Az adatbázisból csak a legszükségesebb adatot kérjük el (és nem select * from
fulltable), a kimenő sávszélességet a maximálisra növeljük, a webkiszolgálót optimalizáljuk, kimeneti
tartalmat tömöríttetjük. Szóval egy kisebb IT hadsereget mozgósíthatunk, de lehet, hogy nem érünk el
számottevő eredményt. Legkésőbb ekkor kerül elő a gyorsítótárazás, a tömörítés, az egy oldalhoz
tartozó további requestek számának csökkentése. Most csak azokra az adatátviteli pontokra
fókuszáljunk, ami az MVC-vel kapcsolatban szóba jöhet.

 Az adatmodell cache-elése. Ezzel tehermentesíthetjük az SQL szervert az ismétlődő


lekérdezésektől. A ritkán változó törzsadat jellegű táblázatokért nem érdemes az SQL szervert
zaklatni, főleg akkor nem, ha az egy másik gépen található és nem a webkiszolgálóval van
hardver-lakótársi viszonyban. Erre szolgálnak megoldásként az ASP.NET Cache és Session
lehetőségei. Az ilyen jellegű cache-elés alapesetben a memóriát fogyasztja.
 Az elkészült, a View renderelő által generált HTML oldalak gyorsítótárazása. Ezzel a
processzort tudjuk tehermentesíteni, mivel a View renderelése számításigényes munka.
Amint láttuk, az MVC a View-ban található sablonnyelv tartalmából egy ideiglenes dll jön létre.
A dll létrehozása után következő oldalkiszolgálások ezeket az ideiglenesen összeállított
assembly-k futtatását igénylik. Ez már jóval kevesebb CPU időt vesz igénybe, mint az első
renderelés, amikor a View dll-ek elkészülnek, de még mindig gépigényesebb mintha a kész
HTML tartalmat szolgálnánk ki. Mivel a View alapján készült eredmény egy HTML szöveg, ezt
is lehet gyorsítótárazni. Ez a cache-elési mód fogyaszthat memóriát vagy háttértárat, attól
függően, hogy a kész HTML tartalmat hol tároljuk ideiglenesen.
 A szétaprózva tárolt, de azonos célú fájlok egységesítése. A sok CSS, JS és ikonméretű
képfájlról van szó. Egy kortárs, még nem optimalizált webalkalmazás nagyságrendileg 50-200
ilyen fájlt tölt le egyetlen oldal kiszolgálása alkalmával, holott elég lenne 1+3 darab is. Egy
HTML, egy CSS, egy JS és egy képfájl (sok kis képpel mozaikosan). Legalábbis ez lenne a
kívánalom.
 A cache-elési "helyszínek" szétválasztása. Lehet gyorsítótárazni a szerveren, a köztes proxy
szerveren, a böngésző memóriában, a kliens fájlrendszerben. Célszerű a legritkábban változó
tartalmakat a kliens gépen fájlrendszerben tároltatni. Lehet gyorsítótárazni a komplett HTML
10.1 Reakcióképesség, gyorsítás, minimalizálás. - Az OutputCache 1-298

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.

Nézzük a függőséget meghatározó paramétereket először. A példakódok a CacheDemoController


actionjeiben vannak.

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>

<!—további kódok, ld példa -->

<tr>
@Html.Action("CacheTestChild1", new { id = 1 })
</tr>
<tr>
@Html.Action("CacheTestChild1", new { id = 2 })
</tr>
<tr>
@Html.Action("CacheTestChild1", new { id = 3 })
</tr>

Egy közös actiont használnak.

[OutputCache(Duration = 10, VaryByParam = "Id")]


public ActionResult CacheTestChild1(int? id)
{
return PartialView("CacheTestChild", CacheDemoModel.GetModell(id ?? 1));
}

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

A modell többi része a megszokott osztály.

public static CacheDemoModel GetModell(int id)


{
if (datalist == null) datalist = new Dictionary<int, CacheDemoModel>();
if (!datalist.ContainsKey(id))
{
datalist.Add(id, new CacheDemoModel()
{
Id = id,
FullName = "Tanuló " + id,
});
}

var dl= datalist[id];


dl.SetSelectTime();
return dl;
}

public void SetSelectTime()


{
this.SelectTime = DateTime.Now;
}

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)

Ezt a viselkedést a VaryByParam="none"–al tudjuk kikapcsolni. Ennek eredményét mutatja a következő


ábra, egyben bizonyítja is, hogy az előző ábra esetében még működött a VaryByParam. A cache minden
esetben a Tanulo 1 modell alapján elkészült első child action HTML eredményét adja vissza. Az Id értéke
már nem játszott szerepet
10.1 Reakcióképesség, gyorsítás, minimalizálás. - Az OutputCache 1-301

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

Talán felfedezhető, hogy a User-Agent a böngészőről és a környezetéről hordoz információkat.

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

Ez a paraméter a HTTP fejléc Accept-Encoding alapján határozza meg a cache bejegyzések


elkülönítését. Szintén használható a pontosvesszős felsorolás, de itt az encoding vesszővel elválasztott
típusok nevét jelenti. A fenti táblázatból ezzel a "gzip" és a "deflate" neveket lehet megcélozni. Ezzel
el lehet érni, hogy más-más tartalomtömörítő képességű böngésző számára más cache irányelveket
határozzunk meg.

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.

[OutputCache(Duration = 120, VaryByCustom = "minorversion;majorversion")]


public ActionResult CacheTestCustom(int? id)
{
return View("CacheTestHeader",CacheDemoModel.GetModell(id ?? 1));
}

A global.asax-ban a GetVaryByCustomString metódust tudjuk felülbírálni, hogy új cache bejegyzés


nevet (kulcsot) határozzunk meg. Ha nem bíráljuk felül az ASP.NET csak egy custom értéket ismer a
"browser"-t, ami böngészőtípusonként határoz meg cache kulcsot. (Browser.Type)

public override string GetVaryByCustomString(HttpContext context, string custom)


{
switch (custom)
{
case "browser": //<- az Application ős is ezt csinálja, tehát erre itt nincs is szükség
return context.Request.Browser.Type;
case "minorversion":
return "BrowserFoVer=" + context.Request.Browser.MinorVersion;
case "majorversion":
return "BrowserAlVer=" + context.Request.Browser.MajorVersion;
case "mobiledevice":
return "BrowserMobile=" + context.Request.Browser.IsMobileDevice;
default:
return base.GetVaryByCustomString(context, custom);
}
}

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.

[OutputCache(Duration = 120, VaryByCustom = "browser")]


10.1 Reakcióképesség, gyorsítás, minimalizálás. - Az OutputCache 1-303

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.

OutputCacheLocation Viselkedés Cache-Control


Enum érték
Any Az összeállított HTML tartalom bárhol tárolható. A public
böngészőben a proxy szerveren és a web
szerveren is. Ez az alapértelmezett működés.
Client A tartalom csak a böngészőben kerül private
gyorítótárazásra.
Downstream A cache-elt tartalom tárolható a böngészőben és a public
köztes proxy szerveren is.
Server Csak a szerveren tárolódik. no-cache
ServerAndClient Szerveren és böngészőben is, de a proxy private
szerverben nem tárolódhat
None Nincs tárolás, nincs cache. no-cache

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 public, max-age=120


Date Tue, 11 Jun 2013 19:12:07 GMT
Expires Tue, 11 Jun 2013 19:14:07 GMT
Last-Modified Tue, 11 Jun 2013 19:12:07 GMT

Könnyen azonosíthatóak a cache-elésre vonatkozó értékek. A "max-age=120" és az Expires mínusz


Date is 120 másodperc. A következő kivonatot adta egy OutputCacheLocation.None értékkel:

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));
}

A cache profilokat a web.config fájlban kell definiálni név szerint.

<system.web>

<caching>
<outputCacheSettings>
<outputCacheProfiles>
<add name="OtPercVaryById" duration="300"
varyByParam="Id" />
<add name="HatvanMasodpercVaryByNone" duration="120"
varyByParam="none" />
</outputCacheProfiles>
</outputCacheSettings>
</caching>
</system.web>

A CacheProfile meghatározásával elkerülhető a Duration és VaryByParam paraméterek kötelező


megadása. Ha már ránéztünk a web.config-ra, az egész output cache – például fejlesztés alatt –
letiltható globálisan ezen a módon:

<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

Partial (View) Cache

A VaryByParam vizsgálatánál elbújva használtuk azt a lehetőséget, hogy az oldal létrehozásában


főszerepet játszó View-ban további partial View-k voltak beágyazva úgy, hogy csak a kapcsolódó child
actionök használták az OutputCache attribútumot valódi célra. Ott az volt a beállítás, hogy a fő View-
val kapcsolatban levő actionön az output cache-t kikapcsoltuk és a child actionön volt csak 10
másodperces lejárati idő meghatározva. Ez volt a két action beállítása:

[OutputCache(NoStore = true, Duration = 0)]


public ActionResult CacheTestParent(int? id)
{
return View();
}

[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.

Layout Layout Fejléc Action Duration = 600


View action Felső rész Action Duration = 60
Duration = 0 Középső rész Action (a lyuk a fánkon) Nincs cache attr.
10.2 Reakcióképesség, gyorsítás, minimalizálás. - Az adat Cache 1-306

Alsó rész Action Duration = 120


Layout Lábléc Action Duration = 3600

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

10.2. Az adat Cache

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

public ActionResult TipicalSearchAction(string category, string name)


{
var model = dataService.GetModelByCategory_Name(category, name);
return View(model);
}

Í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.

A példakódot rövidítendő, a szűrőfeltétel paramétere legyen csak az "id", és a modelleket az id értékei


szerint tároljuk el a cache-ben. A következő példában a modell cache kezelése egy actionfilterbe van
ágyazva. A futás lépései:

 Az első futás során az actionfilter OnActionExecuting metódusa megpróbálja a cache-ből


elővenni a modellt, de még nem találja.
 Mivel nincs modell az action kénytelen az adatbázisból előállítani egyet. Ezt a modellt
továbbkülni a View-nak, ami alapján majd elkészül a HTML válasz.
 A View futása után a modell még rendelkezésre áll, ezt az actionfilter OnResultExecuted
metódusa átveszi és a paraméterekből képzett cache index/kulcs alapján berakja a Cache-be.
 A következő action futása előtt, ha az action paraméterei azonosak, az OnActionExecuting
metódusa megtalálja a cache-elt modellt és a ViewData.Model-be tölti. Ez azt jeleni, hogyha
az action semmi különöset sem csinál vele, akkor a View számára ez lesz a modell példány.
 Elindul az action és megvizsgálja, hogy a ViewData.Modell ki van-e töltve. Mivel az
actionfilter ezt már megtette, nincs is semmi dolga, a View alapján elkészül az oldal.
10.2 Reakcióképesség, gyorsítás, minimalizálás. - Az adat Cache 1-308

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.

[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]


public class ModelDataCacheFilterAttribute : ActionFilterAttribute
{
//request paraméter nevek.
private readonly string[] _splittedRequestparams;
//Elkülönítés actionönként vagy modelnevenként
private string _cacheKeyPrefix;

public ModelDataCacheFilterAttribute(string modelName, string requestParams)


{
if (string.IsNullOrWhiteSpace(requestParams))
throw new ArgumentException("Legalább egy request paramétert meg kéne adni...");
_splittedRequestparams = requestParams.Split(new[] { ',', ';' },
StringSplitOptions.RemoveEmptyEntries);
if (!string.IsNullOrWhiteSpace(modelName))
_cacheKeyPrefix = modelName + ".";
}

//Model megszerzése a cache-ből


public override void OnActionExecuting(ActionExecutingContext filterContext)
{
base.OnActionExecuting(filterContext);
var cachekey = GetCacheKey(filterContext);
var model = filterContext.HttpContext.Cache[cachekey];
if (model == null) return;

//Post esetén a cache érvénytelenítése.


if (filterContext.HttpContext.Request.HttpMethod == "POST")
{
filterContext.HttpContext.Cache.Remove(cachekey);
filterContext.Controller.ViewData["originalModel"] = model;
return;
}
//Modell átadása a View számára.
filterContext.Controller.ViewData.Model = model;
}

//Model tárolása a cache-ben


public override void OnResultExecuted(ResultExecutedContext filterContext)
{
base.OnResultExecuted(filterContext);
var result = filterContext.Result as ViewResultBase;
if (result == null || result.Model == null) return;

var cacheKey = GetCacheKey(filterContext);


var model = filterContext.HttpContext.Cache[cacheKey];
if (model != null) return; //A cache elem még érvényes

//Öt perc sliding expiration


filterContext.HttpContext.Cache.Insert(cacheKey, result.Model, null,
Cache.NoAbsoluteExpiration, new TimeSpan(0, 5, 0));
}

private string GetCacheKey(ControllerContext ccontext)


{
10.2 Reakcióképesség, gyorsítás, minimalizálás. - Az adat Cache 1-309

if (_cacheKeyPrefix == null) //Nincs modellnév->kontroller+action


{
_cacheKeyPrefix = string.Join("+", ccontext.RouteData.Values
.Where(r => r.Key == "controller" || r.Key == "action")
.Select(s => (s.Value ?? s.Key).ToString())) + ".";
}

var hcontext = ccontext.HttpContext;


var q = _splittedRequestparams.Where(s => ccontext.RouteData.Values.ContainsKey(s))
.Select(s => (ccontext.RouteData.Values[s] ?? "").ToString());
var q2 = _splittedRequestparams.Where(s => hcontext.Request[s] != null)
.Select(s => hcontext.Request[s].ToString());

return _cacheKeyPrefix + string.Join(".", q.Union(q2));

}
}

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 OnActionExecuting metódusa megpróbálja megszerezni a cache-elt modellt. Abban az esetben, ha


egy post request érkezett a modellt még átmásolja egy ViewData elembe, de a cache-ből törli a
bejegyzést, feltételezve azt, hogy a modell tartalmi változása miatt amúgy is érvénytelen lesz a
cache-bejegyzés. (A példa esetében ez ki van zárva, mert az id egyben a modell entitásazonosítója).
Get request esetén normál modellfeltöltés zajlik le. Ezt tudja majd átvenni az action a
ViewData.Model-en keresztül.

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);
}

Látható, hogy a ViewData["originalModel"]-ből megpróbálja átvenni a modellt, ha nincs, akkor sajnos


egy adatbázis lekérés szükséges. Ezek után megtörténik a modell update-elése a postadatokkal
(TryUpdateModel). Validációs hiba esetén megy vissza a modell a View számára.

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.Insert(cacheKey, result.Model, null,


Cache.NoAbsoluteExpiration, new TimeSpan(0, 5, 0));

... =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:

Insert(string key, object value, CacheDependency dependencies, DateTime


absoluteExpiration, TimeSpan slidingExpiration, CacheItemPriority priority,
CacheItemUpdateCallback onUpdateCallback)

 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

 dependencies – A gyorsítótárazott elemek számára, az időn kívül egyéb függőségeket is


beállíthatunk egy CacheDependency objektumon keresztül
o Fájlfüggőség. CacheDependency(string fájlnév vagy könyvtárnév) vagy
CacheDependency(string[] fájlnevek) konstruktorverzióval a megadott fájl vagy
mappa megváltozásától (módosítási dátumától) fog függeni a cache elem
érvényessége. Ha a fájl nem létezik, akkor is elkészül a bejegyzés, és érvénytelenné
fog válni, amint a fájlt létrehozzuk. Alapértelmezetten az aktuális időponttól kezdi a
figyelést, és akkor lesz érvénytelen az elem, ha ennél újabbra változik a fájl vagy
mappa módosítási dátuma. Egy további 'start' konstruktorparaméterrel
megadhatjuk ezt az időpontot.
o Függőség más cache elem(ek)től. A CacheDependency(null, string[] cache-kulcsok) s
a tömbben felsorolt cache elemektől fog függeni az aktuálisan létrehozandó elem
érvényessége. Így a cache-elt elemünk függ a sajátmagán beállított függőségektől, de
ha a felettes cache-bejegyzés érvénytelenedik, akkor az magával vonja a a mi függő
cache elemünk érvénytelenedését is.
o Függőség SQL lekérdezéstől. Ez ugyan az a képesség, amit az OutputCache-nél
láttunk.
 absoluteExpiration – A cache elem lejárati ideje pontosan megadva a jövőben. Az előbbi
példakódban a Cache.NoAbsoluteExpiration statikus értékkel jelezhetjük, hogy nem
kívánunk élni ezzel a lehetőséggel.
 slidingExpiration – A csúszólejárat értelme, hogyha gyakran van szükség egy cache
bejegyzésre, akkor valószínűleg a közeljövőben is szükség lesz rá. -> Maradjon csak
nyugodtan a cache-ben. A működés logikája az, hogy az itt megadott időeltolás (TimeSpan)
múlva lesz a lejárat, ha nem történik hozzáférés a cache elemhez. A slidingExpiration értéket
megfelezve értelmezi. Ha az első félidőben történik hozzáférés az elemhez, akkor nem
csúsztatja az időt. (hype idő…). A második félidőben történt hozzáférés esetén a lejárati időt
meghosszabbítja az eredetileg megadott slidingExpiration értékkel. Ha nem kívánjuk
használni, adjuk meg Cache.NoSlidingExpiration értéket.
 priority – Az egymással függőségben levő cache bejegyzések eltávolítási sorrendjét lehet
szabályozni ezzel az Enum értékkel. Alapértelmezetten a függő elemek lesznek először
felszámolva és utána a felettesük. A felszámolási sorrend értelmét adja a következő
paraméter:
 onUpdateCallback – Ez a legérdekesebb képessége (számomra). A cache elem
érvénytelenítésekor az itt megadott visszahívási metódust a szóban forgó, lejárt elemmel
meghívja. Ekkor még döntést hozhatunk, hogy mi történjen a tárolt objektummal. Csak
ötletadásként: visszatehetjük a Cache-be, mert úgy ítéljük meg, hogy még jó helyen van ott.
Eltárolhatjuk fájlba, adatbázisba, lassabb cache-be. Statisztikát készíthetünk arról, hogy a
cache elemek a megadott lejárai idő szerint mennyire voltak hasznosak. A cache-bejegyzési
adaptív algoritmusunk pedig esetleg rövidebb lejárati időt ad legközelebb a kisebb találati
esélyű elemek számára.

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ézzünk egy nagyon egyszerű képzeletbeli példát:

Oldal neve/funkciója Igényelt JS könyvtárak modulcsoport neve


Nyitó oldal jquery, jquery UI jquery_ui
Hírek oldalak jquery, jquery UI, jquery-validation jqueryvalui
Bemutatkozó oldal jquery jquery_ui
Termékek oldalak jquery, jquery UI, jquery-validation jqueryvalui

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:

/// <reference path="jquery.validate.js" />

Az gondolom érzékelhető, hogy a JS fájlok egybemásolása nem jelent különösebb erőforrásigényt, a


minifikálás viszont egy kicsivel többet. Ezért, ha nem áll rendelkezésre egy minifikált változat, készítsük
el manuálisan. Erre számtalan online alkalmazás is elérhető.

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")
}

A fenti sor a _ Layout.cshtml-ben levő @RenderSection("scripts", required: false) helyére


fogja injektálni az eredményét.

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

A másik módon, felülbírálva a web.config-os beállítást, így lehet bekapcsolni a bunlingot:

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>

Az eredménye az összefűzött JS tartalom (csak egy képkivágás):


10.3 Reakcióképesség, gyorsítás, minimalizálás. - A Bundling 1-315

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.

Ennek megvan az MVC-ben használható megvalósítása, ami a http://www.dotlesscss.org/ oldalon


megtekinthető és NuGet csomagban elérhető. Az ilyen extra képességeknek az ára, hogy a kódot
értelmezni, fordítani kell ahhoz, hogy a böngésző számára emészthető CSS fájlt kapjunk (+ meg kell
tanulni még egy nyelv szintaxisát). Most nem mennék bele ennek a nyelvezetébe, részleteibe, hanem
az a fontosabb, hogy hogyan illeszthető ez vagy más fordítóprogram a Bundle feldolgozásába.

A ScriptBundle és a StyleBundle is a Bundle ősből származik és valójában csak annyi a specialitásuk,


hogy példányosítanak egy IBundleTransform interfészt megvalósító osztályt. A StyleBundle például
egy CssMinify-t, ami elvégzi a CSS-hez illeszkedő minifikálást. Ez a transzformációs osztály, egymaga
bekerül a Bundle ős belső listájába. A bundle működése nem más, minthogy annak a listának az elemeit
aktivizálja, amik elvégzik a rájuk tartozó transzformációt, például a CSS minifikálást. Ahhoz, hogy egyéb
transzformációs műveletet (most LESS fordítást) el tudjunk végezni, mindössze annyit kell csinálnunk,
hogy ebbe a listába szúrunk egy újabb IBundleTransform –ot megvalósító LESS fordító osztályt.

var lessbundle = new StyleBundle("~/Content/less");


lessbundle.Include("~/Content/mini.less");
lessbundle.Transforms.Insert(0, new LessTransform());
bundles.Add(lessbundle);

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ó.

A LessTransform megvalósítása, elég egyszerűen a bejövő response.Content szöveges tartalmát elküldi


a LESS fordítónak, majd annak az eredményét visszatölti a Content-be. (Ez megy tovább a CssMinify-
nek)

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

A böngészőhöz egy minifikált CSS szövegtartalom fog megérkezni a behelyettesített szín


konstansokkal:

#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:

<link href="/Content/mini.less" rel="stylesheet"/>

A bundle esetében ezekre a kikommentezett szakaszokra nincs szükség a web.config-ban:

<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

11. Real world esetek

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.

11.1. Többnyelvű alkalmazás

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ű?

 Az alkalmazás vezérlői, gombjai linkjei, menüpontjai. Azaz a felület, a User Interface.


 A tartalom. Ez egy ingoványos terület, attól függően, hogy mit értünk "tartalom" alatt.
o Lehetnek a mértékegységek, a dátumformátumok a pénznemek, a legördülő listák
elemei, az enumok alapján megjelenő szövegek, stb.
o Külön problémakör a taxonómiai elemek nyelvi variánsai. Ide tartoznak a
termékkategóriák, csoportok és egyéb besorolási nevek.
o Lehetséges, hogy a felhasználó azt érti alatta, hogy a cégbemutató oldal is több
nyelven jelenjen meg. Így nyelvi variánsainak is kell lennie, az azonos nevű, azonos
menüpontból elérhető dinamikus oldalak tartalmának.

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:

[Display(Name = "FullNameLabel", ResourceType = typeof(Resources.UILabels))]


public string FullName { get; set; }

Létre kellett hozni ehhez egy resource fájlt a Resources mappába.


11.1 Real world esetek - Többnyelvű alkalmazás 1-320

Az ASP.NET Web Forms alkalmazásoknál az App_GlobalResources és az App_LocalResources


különleges jelentőséggel bír a resources fájlok számára, de az MVC alkalmazásoknál nincs szükség
ezekre a mappákra. Sőt kerülendő, hogy ezekbe tegyük a resource fájlokat.

Meg kellett adni a FullNameLabel bejegyzéshez a szöveget és át kellett állítani a hozzáférhetőséget


"Public"-ra.

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.

Nyelv Nyelvi kód fájlnév


Alapértelmezett - UILabels.resx
Magyar hu, vagy Hu-hu UILabels.hu.resx, UILabels.hu-HU.resx
Angol en, vagy en-GB, en-US, UILabels.en.resx, UILabels.en-GB.resx, UILabels.en-US.resx
stb

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).

Miután létrehoztam egy UILabels.en.resx változatot ilyen mappaszerkezetet kaptam:

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;

public class UILabels {


. . .
public static string FullNameLabel {
get {
return ResourceManager.GetString("FullNameLabel", resourceCulture);
}
}
}
}

Így a statikus propertyt közvetlenül is elérhetjük a razor kódból:

@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.

Az alkalmazás alapértelmezett nyelvi beállítását a web.config-ban lehet beállítani. Fixen magyarra


állítani például így lehet:

<system.web>
<globalization requestEncoding="utf-8" responseEncoding="utf-8" fileEncoding="utf-8"
culture="hu-HU" uiCulture="hu"/>

A "culture" határozza meg többek között a dátum formátumot, az alapértelmezett pénznemet, a


tizedes pontot vagy vesszőt, azaz amit a számítógép nyelvi beállításaiban, a Vezérlőpulton is be lehet
állítani. Az uiCulture pedig a resource fájl kiválasztására hat. Azonban ez a beállítás azt jelenti, hogy
csak a magyar erőforrásfájlt fogja használni és nem vesz figyelembe semmit.
11.1 Real world esetek - Többnyelvű alkalmazás 1-322

Célszerűbb, ha automatikusan határoztatjuk meg a nyelvi változatot. Ilyenkor az lesz a kiválasztott


nyelv, ami a böngészőben be van állítva:

<system.web>
<globalization requestEncoding="utf-8" responseEncoding="utf-8" fileEncoding="utf-8"
culture="auto" uiCulture="auto"/>

A böngésző a request fejlécben tájékoztatja a webkiszolgálót a számára preferált nyelvekről.

GET /Multilanguage HTTP/1.1


Host: localhost:18005
Accept-Language: hu,de;q=0.8,en-us;q=0.5,en;q=0.3

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.

var langroute = new LocalizedRoute("{lang}/{controller}/{action}/{id}",


new { lang = "en", controller = "Home", action = "Index", id = UrlParameter.Optional });

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

public class LocalizedRoute : Route


{
public LocalizedRoute(string url, object defaults)
: base(url, new RouteValueDictionary(defaults),
new RouteValueDictionary( new { lang = "[a-z]{2}" }), new MvcRouteHandler())
{ }

public override VirtualPathData GetVirtualPath(RequestContext requestContext,


RouteValueDictionary values)
{
if (!values.ContainsKey("lang"))
values["lang"] = Thread.CurrentThread.CurrentCulture.TwoLetterISOLanguageName;

return base.GetVirtualPath(requestContext, values);


}
}

Az ősosztályhoz átpasszolt konstruktorparaméterében levő anonymous osztályocska a "lang" route


szakaszok megszorításait határozza meg regular expression-nel. Jelen esetben a "lang" csak két
kisbetűt tartalmazó kód lehet.

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.

public class LanguageModel


{
public const string DefaultLanguage = "en";
public static string[] AppLanguages = new string[] { "hu", "en", "de" };

public static string GetAvailableOrFallback(string inlang)


{
return AppLanguages.Contains(inlang) ? inlang : DefaultLanguage;
}
}

A _Layout.cshtml-be beszúrt szakasz, ami a fapados nyelvválasztót készíti el:

<section id="langselector" style="text-align: right">


@foreach (var lang in MvcApplication1.Models.LanguageModel.AppLanguages)
{
<span>@Html.ActionLink(lang, "ChangeLang", "Multilanguage",
new { langcode = lang }, null)</span>
}
</section>

A képen látszanak a nyelvválasztó "hu en de" linkek. Ezek


lehetnének zászlócskák is egy normál alkalmazásban.

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.

public class MultilanguageController : Controller


{
public ActionResult Index()
{
return View(ValidationMaxModel.GetModell(1));
}

public ActionResult ChangeLang(string langcode)


{
var validlangcode = LanguageModel.GetAvailableOrFallback(langcode);

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);
}
}

return RedirectToRoute(new { lang = validlangcode, controller = "Home", action = "Index" });


}
}

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.

public void Application_BeginRequest(object sender, EventArgs e)


{
var currentContext = new HttpContextWrapper(HttpContext.Current);
var routeData = RouteTable.Routes.GetRouteData(currentContext);
if (routeData == null || routeData.Values.Count == 0) return;

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.

Abban az esetben, ha az URL-ből meghatározható a nyelvi kód (hu/Home/Index), akkor is át kell


passzírozni a szűrőn, mert az URL-be azt írnak, amit akarnak.

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

Végezetül a fenti képek előállításáért felelős View:

<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

11.2. Az alkalmazás modularizálása. Az Area.

Mikorra eljutottam ehhez a fejezethez, kezdett rendetlenség lenni a példakódok Controllers


mappájában annak ellenére, hogy további almappákat használtam a rendszerezéshez. A View-s mappa
a kötött mappaelnevezéseivel szintén jól meghízott. A Models nem nőtt túl nagyra, mivel alig
használtam modellosztályokat, de egy valódi alkalmazásnál már ez is igen méretes lenne.

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.

Szekció, funkcionális csoport Tartalom


Fő modul, maga az alkalmazás gyökere Nyitólapok, újdonságokkal, hírekkel,
eseményekkel. Főmenü.
Adminisztrációs oldalak Felhasználó kezelés, jogosultságok.
Alap/törzsadatok beállítása
Közös tanulmányi oldalak Verseny kiírások, fakultációk, szakkörök.
Házirend. Órarendek. Vizsgarend
Diákélet Beszámolók, osztályok, képgalériák,
eszmecserék.
Bemutatkozó és egyéb statikusabb oldalak Történelmünk, tanáraink, céljaink.

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.

"Admin"-t adva area névnek, ezt a struktúrát kapjuk:

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:

public class AdminAreaRegistration : AreaRegistration


{
public override string AreaName { get { return "Admin"; } }

public override void RegisterArea(AreaRegistrationContext context)


{
context.MapRoute(
"Admin_default",
"Admin/{controller}/{action}/{id}",
new { action = "Index", id = UrlParameter.Optional }
);
}
}
11.2 Real world esetek - Az alkalmazás modularizálása. Az Area. 1-329

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:

- Miért kell regisztrálni az area-t?


- Mikor és hogyan fog ez az Area regisztráció funkcioná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.

A második kérdésre a választ a global.asax- ban találjuk meg:

private void Application_Start()


{
AreaRegistration.RegisterAllAreas();

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:

AreaRegistration.RegisterAllAreas("Areának átadott típusmentes paraméter");

A paramétert a context.State propertyben tudjuk átvenni:

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

A demóalkalmazáson belül az area-ban elhelyezett HomeController névtere ez:

namespace MvcApplication1.Areas.Admin.Controllers

A route mappeléseknek van egy DataTokens gyűjteménye, amiben speciális meghatározásokat


helyezhetünk el az adott mappeléssel kapcsolatban. Az egyik ilyen már látott, speciális bejegyzés a
"Namespaces". Ezzel a route mappelést az adott névtérhez vagy névterekhez köthetjük. Mivel a
többnyelvű alkalmazásokat bemutatva létrehoztunk egy speciális route mappelést, így most azt is be
kell állítani az alapalkalmazás RouteConfig osztályában.

var langroute = new LocalizedRoute("{lang}/{controller}/{action}/{id}",


new { lang = "en", controller = "Home", action = "Index", id = UrlParameter.Optional});

langroute.DataTokens = new RouteValueDictionary


{
{"Namespaces", new string[] {"MvcApplication1.Controllers"}}
};
routes.Add("LocalizedRoute", langroute);

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:

public override string AreaName { get { return "Admin"; } }

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:

public override void RegisterArea(AreaRegistrationContext context)


{
object fogadottparameter = context.State;
context.MapRoute(
"Admin_default",
"Admin/{controller}/{action}/{id}",
new { action = "Index", id = UrlParameter.Optional },
new string[] { "MvcApplication1.Areas.Admin.Controllers" }
);
}
11.2 Real world esetek - Az alkalmazás modularizálása. Az Area. 1-331

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:

public class AdminAreaRegistration : AreaRegistration


{
public const string Area = "Admin";
public override string AreaName { get { return Area; } }

public override void RegisterArea(AreaRegistrationContext context)


{
object fogadottparameter = context.State;
context.MapRoute(
AreaName + "_default",
AreaName + "/{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional},
new string[] { "MvcApplication1.Areas.Admin.Controllers" }
);
}
}

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.

1 - @Html.ActionLink("Alap projekt Home", "Index", "Home", new { area = "" }, null)


<br>
2 - @Html.ActionLink("Admin area Home", "Index", "Home")
<br>
3 - @Html.ActionLink("Admin area Felhasználók", "Index", "Felhasznalok",
new { area = AdminAreaRegistration.Area }, null)
<br>

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 2. link visszamutat az Admin area nyitólapra.


11.2 Real world esetek - Az alkalmazás modularizálása. Az Area. 1-332

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:

Admin area menüi:


<br>
1 - @Html.ActionLink("Alap projekt Home", "Index", "Home")
<br>
2 - @Html.ActionLink("Admin area Home", "Index", "Home", new { area = "Admin" }, null)
<br>
3 - @Html.ActionLink("Admin area Felhasználók", "Index", "Felhasznalok", new { area = "Admin" }, null)
<br>

Ugyan ez érvényes a Html.BeginForm, Html.Action és a további linkekkel dolgozó helperekre is.

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

Fő projekt areabaselayout mainlayout

Area Értekesítés CRM

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

11.3. Mobil nézetek, View variánsok

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.

Három megközelítést említenék:

 Az egyik, amikor a megjelenítési rétegeket elszeparáljuk. Készítünk egy komplett oldalszekciót


a csökkent képességű, "mobil" eszközök számára és egy másikat a normál megjelenítővel bíró
számítógépek számára. Sok esetben az ilyen oldalszekciókat annyira elszeparálják, hogy még
az URL-jük is más. (pl.: www.index.hu / m.index.hu). Ilyen esetben az "eltévedt" böngészőt
rendszerint átirányítják a számára készített URL gyökérhez, ha nem a megfelelőt célozta meg.

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.

A döntési pont a Request.Browser adatainak vizsgálata. Ennek a paramétereiben viszont ne bízzunk


100%-osan. Ott van például a kecsegtető ScreenPixelsWidth tulajdonsága, amiben még soha nem
találtam mást, mint 640-et. Az IsMobileDevice és a Browser tulajdonsága viszont pontos.

Ez a Request.Browser objektum a nyers böngésződetektálás eredményét hordozza. A detektálás alapja


a .browser fájlok listája, amiket a machine.config mappájából nyíló Browsers alatt találhatunk meg.
(pl.: Windows\Microsoft.NET\Framework64\v4.0.30319\Config\Browsers). A .browser fájlok belső,
XML definíciója tartalmazza, hogy mikre képes a böngésző, és hogy egyáltalán hogyan lehet
megállapítani, hogy melyik böngészőről van szó a request User-Agent –je alapján.

Ahogy a többnyelvű alkalmazásban is javasoltam a nyelvválasztó linkek esetében: csináljunk most is


egy átkapcsolót, amivel majd a felhasználó átkapcsolhatja, hogy milyen eszközbeállítás szerint akarja
nézni az oldalainkat. Így nincs kiszolgáltatva annak, hogy az ASP.NET miként értelmezte az ő
böngészőjének/eszközének a tulajdonságait. Ez hasznos lesz majd a rákövetkező részben is, ahol majd
az eszközspecifikus megjelenítést tanulmányozzuk.

public ActionResult Index()


{
return View();
}

public RedirectResult ChangeBrowserMode(bool mobile, string returnUrl)


{
if (this.Request.Browser.IsMobileDevice == mobile)
this.HttpContext.ClearOverriddenBrowser();
else
this.HttpContext.SetOverriddenBrowser(mobile ?
BrowserOverride.Mobile : BrowserOverride.Desktop);
return this.Redirect(returnUrl);
}

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

Ez a kép egy Opera mobil böngészővel készült állapotot


mutatja, amikor a normál böngésződetektálás felül lett
bírálva. Az "Request alapján" oszlop a nyers Browser
objektumból származik.

A HttpContext-ből a GetOverriddenBrowser()metódussal tudjuk megszerezni a szimulált


böngészőadatokat. Szerencsére az MVC nem a Request.Browser-t, hanem ha van, akkor a szimulált
User-Agent értékkel dolgozik. Érdemes beszerezni a teszteléshez egy mobil emulátort.

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.

Hozzunk létre egy Partial View-ból két variánst, például


IndexPartial.cshtml és IndexPartial.Mobile.cshtml néven, és tegyünk
bele valamilyen megkülönböztethető tartalmat, ami visszautal a View
céljára.

IndexPartial IndexPartial.Mobile
<h3>Desktop partial View</h3> <h3>Mobile partial View</h3>

Hogy aktivizálódjanak a Partial View-k kell egy-egy Html.Partial az index.cshtml és az


index.Mobile.cshtml-be is. Mindkettőbe írhatjuk ugyanazt a helpert, és nem szükséges (de lehet) a
'.Mobile' verzióra hivatkozni az Index.Mobile.cshtml fájlból :

@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.

Jelenleg a jobb oldali képen látható eredménynél tartunk egy iPhone


emulátorban nézve, átkapcsolva az előzőleg megírt browser kapcsolóval
desktop üzemre. Előfordulhat olyan helyzet, hogy a partial View annyira
egyszerű, hogy nem érdemes két variációt készíteni belőle. Mivel a
Html.Partial –al elég csak az alapverzióra hivatkozni így nem okoz gondot,
ha nincs mobil változat a partial View-hoz.

Nem mindig elég két variáns. Lehetséges, hogy az egyik eszköztípuson


kicsit mást szeretnénk megjeleníteni. Mutattam, hogy a tableteket sem
különbözteti meg, pedig a touch képesség megléte vagy hiánya némileg
más interakciókezelést igényelhet. Azt, hogy egy eszköz esetén mi legyen
a View fájl utótagja a DisplayModeProvider határozza meg. Ennek van
egy belső gyűjteménye, ami IDisplayMode megvalósításokat tárol.
Szerencsére nekünk nem kell egyedileg implementálni, hanem használhatjuk a DefaultDisplayMode
osztályt. Aminek az elnevezése kicsit sántít, mivel nincs is másra szükség, a belső megvalósítás is
minden esetben ezt használja. Ha megnézzük, hogy az egyébként singleton módon elérhető példány
mit tud, egy listán keresztül láthatjuk, hogy az eddig megismert View név utótagok számára
használható névlistát kapunk. Az üres sor szemlélteti azt, amikor nincs névutótagra szükség, ez az
utolsó a listában az igazi "default".

@foreach (var dev in DisplayModeProvider.Instance.Modes)


{
<li>@dev.DisplayModeId</li>
}

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
}

A ContentCondition definíciója egy metódus delegate:

public Func<HttpContextBase, bool> ContextCondition { get; set; }

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

Hol is kéne az új módokat beállítani, mint a global.asax-ban?

//1. iPhone vizsgálattal


DisplayModeProvider.Instance.Modes.Insert(0, new DefaultDisplayMode("Iphone")
{
ContextCondition = (context => context.GetOverriddenUserAgent().IndexOf
("iPhone", StringComparison.OrdinalIgnoreCase) >= 0)
});

//2. Android vizsgálattal


DisplayModeProvider.Instance.Modes.Insert(1, new DefaultDisplayMode("Android")
{
ContextCondition = (context =>
{
var browser = context.GetOverriddenUserAgent();
return browser.IndexOf("android", StringComparison.OrdinalIgnoreCase) >= 0;
})
});

//3. Chrome böngésző vizsgálattal


DisplayModeProvider.Instance.Modes.Insert(2, new DefaultDisplayMode("Chrome")
{
ContextCondition = (context =>
{
var browser = context.GetOverriddenBrowser();
return browser.Browser == "Chrome";
})
});

A fenti display módok kiértékelései önmagukért beszélnek. A kiértékelési sorrend megegyezik a


listaelemek sorrendjével. Mint mindig az első találat győz. Mivel a vizsgálat alanya mindig a
HttpContext-ből érkezik, egyáltalán nincs megkötve, hogy a View utónév-konvenciót csak mobil-nem
mobil megkülönböztetésre használjuk. A 3. példában csak az dönt, hogy a böngésző Chrome vagy nem
az. Hogy mobil eszközről van szó nem is érdekes.

Ha továbbmerészkedünk, az IDisplayMode implementálásával olyan osztályt is készíthetünk, ami a


nyelvi beállításokra reagálva Other.cshtml, Other.hu.cshtml, Other.de.cshtml, Other.il.cshtml View
variánsokra különíti el a feldolgozást. Ezzel olyan különleges helyzeteket is lekezelhetünk, amikor nem
csak a kifejezés fordítása számít, hanem az is hogy balról-jobbra vagy jobbról-balra ír az adott nyelvű
felhasználó. Ez utóbbi írásforma az egész View belső szerkezetére kihat.

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

11.4. Saját Html helperek, modell metaadatok

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.

<input id="textfield" name="textfield" type="text" placeholder="Felhasználó név" />

Kitöltetlen textbox Kitöltött textbox

A következő bemutatók példakódjai az Mvc4HtmlExtensions osztályban vannak.

Első megközelítés: újrahasznosítás

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.

[Display(Name = "FullNameLabel", ResourceType = typeof(Resources.UILabels))]


[Required]
public string FullName { get; set; }

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:

public ActionResult HHtml5Textbox()


{
var model = TemplateDemoModel.GetModell(1);
model.FullName = string.Empty;
return View();
}

A View-ba ennyit kéne írni, hogy működjön:

Új helper: @Html.TextBoxV1For(m => m.FullName)

Az új helper első változata ebben az esetben nagyon egyszerű, mert minden szükséges adat
megszerezhető egyetlen sorban:

public static object TextBoxV1For<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper,


Expression<Func<TModel, TProperty>> expression)
{
ModelMetadata metadata = ModelMetadata.FromLambdaExpression(expression,
htmlHelper.ViewData);
11.4 Real world esetek - Saját Html helperek, modell metaadatok 1-339

return Html.InputExtensions.TextBoxFor(htmlHelper, expression,


htmlAttributes: new RouteValueDictionary() {
{ "placeholder", metadata.DisplayName } });
}

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.

A ModelMetadata.FormLambdaExpression előszedi az expressionben hivatkozott property


metaadatait. Ebből most csak a DisplayName érdekes számunkra, ami tartalmazza a használható
feliratot. Abban az esetben, ha a propertyn definiáltunk a Display attribútummal szöveget, akkor az
abban meghatározott szöveget, ha nem akkor a property nevét tartalmazza. Ezért van az, hogyha nem
definiálunk Display attribútumot a LabelFor a nyers property nevet jeleníti meg. A DisplayName értékét
tovább tudjuk adni a HTML 5-ös placeholder attribútumnak. A feladat megoldva.

Mivel a ModelMetadata példány a Html helper fejlesztések kulcsa, érdemes egy kicsit boncolgatni,
hogy mik érhetők el ezen keresztül.

Attribútum forrású adatok:

 DisplayName – Ezt már láttuk az előbb


 IsRequired – Hasonlóan az előzőhöz, a propertyn levő Required attribútum meglétét jelenti.
 TemplateHint – A UIHintAttribute által megadott DisplayFor vagy EditorFor template neve.
 DataType – A DataType attribútum értéke lesz szövegesen. A jelenlegi példánkban "Text".
 DisplayFormatString – DisplayFormatAttribute: DataFormatString értéke
 EditFormatString – DisplayFormatAttribute: DataFormatString értéke, ha az
ApplyFormatInEditMode true volt.
 NullDisplayText - DisplayFormatAttribute: NullDisplayText értéke. Ezt is használhattuk volna a
placeholder példában, hiszen hasonló a céljuk.
 IsReadOnly – ReadOnlyAttribute vagy az EditableAttribute megléte. (Egymást értelmileg
kizárják)
 ShowForDisplay, ShowForEdit – A ScaffoldColumn attribútum megléte. (A property értékét
nem kell megjeleníteni.)
 RequestValidationEnabled – Az AllowHtml attribútum megléte, azaz beengedhető-e a HTML
tartalom ebbe a propertybe.

Típusinformációk és értékek:
11.4 Real world esetek - Saját Html helperek, modell metaadatok 1-340

 PropertyName – Ez a modell lekérdezett tulajdonságának a neve. Most "FullName".


 Properties – A property típusának a tulajdonságai. Ez akkor hasznos, ha a property típusa egy
osztály és erről szeretnénk információkat megtudni.
 ContainerType – Ez pedig a másik irány. Az aktuális property milyen típusban érthető el, mi a
hordozó osztálya.
 ModelType – Ezen a szinten a Model szó az aktuális objektumot vagy propertyt jelenti, nem
biztos, hogy a modellosztályt. Így a propertyk világában ez a property típusinformációja
(System.Type) lesz.
 Model – Ez sem a nagybetűs modellt jelenti, ha egy propertyről kértünk metainformációkat.
Property szinten ez a propertyben tárolt érték lesz.

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.

Második megközelítés: bővített metainfó

Amint látható volt a ModelMetadata elég sok modellattribútum-definíciót értelmezve, kiértékelve


tárol, de nem mindet. Hogyan lehetne azt megoldani, hogy a modell propertyre új, egyedi
metainformációt tudjunk tenni, amit majd a Html helperben fel tudunk használni? Elsőre azt
gondolnánk, hogy kéne definiálni egy új attribútumot, amit majd reflexióval elérünk. Talán nincs is rá
szükség, mert létezik az AdditionalMetadata attribútum.

[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.

public static object TextBoxV2For<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper,


Expression<Func<TModel, TProperty>> expression)
{
ModelMetadata metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
string phtext;
object phobject;
if (metadata.AdditionalValues.TryGetValue("placeholder", out phobject))
phtext = phobject.ToString();
else
phtext = metadata.DisplayName;
return Html.InputExtensions.TextBoxFor(htmlHelper, expression,
htmlAttributes: new RouteValueDictionary() { { "placeholder", phtext } });
}

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:

[AdditionalMetadata("modell metainfo", "További metainformációk")]


public class TemplateDemoModel

Ezt pedig át tudjuk venni a View-n vagy egy Partial View-n belül:

@if (ViewData.ModelMetadata.AdditionalValues.ContainsKey("modell metainfo"))


11.4 Real world esetek - Saját Html helperek, modell metaadatok 1-341

{
@ViewData.ModelMetadata.AdditionalValues["modell metainfo"]
}

Harmadik megközelítés: Tagbuilder

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:

public static object TextBoxV3For<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper,


Expression<Func<TModel, TProperty>> expression)
{
return TextBoxV3For(htmlHelper, expression, null, null);
}

Sokparaméteres változat.

public static MvcHtmlString TextBoxV3For<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper,


Expression<Func<TModel, TProperty>> expression,
string format, IDictionary<string, object> htmlAttributes)
{
//Metaadatok megszerzése
ModelMetadata metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
//Property path
string name = ExpressionHelper.GetExpressionText(expression);
string fullName = htmlHelper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(name);

//HTML tag építése


TagBuilder tagBuilder = new TagBuilder("input");
tagBuilder.MergeAttributes(htmlAttributes);
tagBuilder.MergeAttribute("type", HtmlHelper.GetInputTypeString(InputType.Text));
tagBuilder.MergeAttribute("name", fullName, true);

//Szöveg formázása
string valueParameter = htmlHelper.FormatValue(metadata.Model, format);
string attemptedValue = null;

//post request adat


ModelState modelState;
if (htmlHelper.ViewData.ModelState.TryGetValue(fullName, out modelState)
&& modelState.Value != null)
attemptedValue = modelState.Value.ToString();

//A textbox tartalmának beállítása


tagBuilder.MergeAttribute("value", attemptedValue ?? valueParameter, true);
//placeholder attribútum <- a főcél
tagBuilder.MergeAttribute("placeholder", metadata.DisplayName);
//id attribútum
tagBuilder.GenerateId(fullName);

//Validációs hibák stílusa


if (modelState != null && modelState.Errors.Count > 0)
tagBuilder.AddCssClass(HtmlHelper.ValidationInputCssClassName);
//Validációs attribútumok
tagBuilder.MergeAttributes(htmlHelper.GetUnobtrusiveValidationAttributes(name, metadata));
11.4 Real world esetek - Saját Html helperek, modell metaadatok 1-342

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

A feladat megoldását a tagBuilder.MergeAttribute("placeholder", metadata.DisplayName)


sor biztosítja, amivel a placeholder attribútumnak adunk értéket. Ezek után már csak az Id attribútum
generálása, a validációs hibák stílusának a beállítása és az unobtrusive validációs attribútumok
feltöltése marad hátra.

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.

11.5. Fájl le- és feltöltés

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

vizsgálandó tényező. Az is elképzelhető, hogy valahogyan a felhasználó olyan fájlnévvel


szeretne feltölteni, amit a Windows fájlrendszere nem enged meg, de az ő rendszere igen.
 A fájlnév duplikációk kérdése. Azért ugyanaz a fájl neve, mert felül akarja írni? Esetleg a
felhasználó teljesen másik tartalmat szeretne tárolni? Neki nem kell tudnia, hogy olyan név
már létezik. Emiatt célszerű lehet a tárolt fájl esetén nem ugyan azt a fájlnevet használni, mint
amit a felhasználó feltöltött.
 A fájlnevekkel való problémakört teljesen ki is kerülhetjük, ha a fájlneveket, a
mappastruktúrát dinamikus módon készítjük el, például Guid alapon. Ahhoz, hogy ez
működjön, készítünk egy adatbázis táblázatot, ami a feltöltött fájl nevét és a fájlrendszerbeli
guid-os elérési útját tárolja és összerendeli. Esetleg szóba jöhet az is, hogy a fájlok tartalmát is
egy táblamezőben tároljuk. Ennek támogatására az MS SQL szerver biztosítja a FILESTREAM
meződefiníciót. A helyzettől függ, hogy számunkra ez előnyös vagy nem.

Valójában máshogy nem is célszerű a fájlkezelést megoldani csak segédadatbázissal együtt. Az


előző problémák egy részét is csak azért említettem meg, hogy lehetőleg elkerüljük a közvetlen
fájlrendszer használatot.

File feltöltés alapok

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:

@using (Html.BeginForm("Upload", "FileAccess", FormMethod.Post,


new { enctype = "multipart/form-data" }))
{
<b>A feltöltendő fájl: </b> <input type="file" name="uploadedfile" />

<input type="submit" value="Feltöltés" />


}

A hozzátartozó action pár:

public ActionResult Upload()


{
return View();
}

[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!"};

var fileName = Path.GetFileName(uploadedfile.FileName);

var path = Path.Combine(Server.MapPath("~/Upload"), fileName);


uploadedfile.SaveAs(path);
}
return RedirectToAction("Index");
}

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.

@using (Html.BeginForm("UploadMulti", "FileAccess",


FormMethod.Post, new { enctype = "multipart/form-data" }))
{

<b>A feltöltendő fájlok egyesével:</b> <br/>


<input type="file" name="uploadedfile[0]" /><br/>
<input type="file" name="uploadedfile[1]" /><br/>
<input type="file" name="uploadedfile[2]" /><br/>
<hr />

<b>HTML 5 lehetőséggel egyszerre</b><br/>


<input type="file" name="uploadedfile"
multiple="multiple"/><br/>

<input type="submit" value="Feltöltés" />


}
A kettő most üti egymást. Vagy csak az egyik módot, vagy csak a másikat használjuk az azonosan
kezdődő name attribútum miatt.

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 HttpPostedFileBase.InputStream egy HttpInputStream objektumot tartalmaz, amivel közvetlenül


is elérhetjük a fájl tartalmát bájtról-bájtra. A HttpPostedFileBase-t elérhetjük a Request.Files[]
tulajdonságon keresztül is.

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

A feltöltéssel kapcsolatban még két tulajdonsággal érdemes tisztába lenni:

requestLengthDiskThreshold – A teljes request méretének az a határa, aminél nagyobb esetén a


tartalom ideiglenes fájlba kerül és nem a memóriában tárolódik.

executionTimeout – A request maximális idejét korlátozhatjuk másodperc alapon. Közvetetten ezzel


lehet szabályozni azt, hogy fájl feltöltésre mekkora az a legnagyobb idő, amit megengedhetünk.

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.

Komplex fájl feltöltés modell, filter, Html helper segítségével.

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:

public class FileModel


{
//A modell állapota
public enum UploadStatus
{
None,
Temp,
Uploaded,
Deleting
}

//Enititás Id és storage fájlnév


public Guid Id { get; set; }

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

public string FileName { get; set; }

[Display(Name = "Leírás")]
[Required]
public string Description { get; set; }

//A post-al feltöltött fájlok


[FileUploadValidation(100, true, "image/png")]
public List<HttpPostedFileBase> Files { get; set; }

[Display(Name = "Fájlméret")]
public long Length { get; set; }

//A fájl elérési újta a storage-ban


public string Path { 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; }

#region InMemory persisztencia


//Listázó, Id alapján lekérő, feltöltő metódusok
#endregion
}

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.

FileUpload validációs attribútum

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]


public sealed class FileUploadValidationAttribute : ValidationAttribute
{
public const string FileUploadValidationName = "FileUploadVA";

private readonly string[] _usableFileTypes;


private readonly Int64 _maxlength;
private bool _isMultiple;

public FileUploadValidationAttribute(Int64 MaxLengthKB = 8192, bool Multiple = true,


string FileTypes = "image/png,image/gif,image/jpg")
{
this._maxlength = MaxLengthKB * 1024;
this._usableFileTypes = FileTypes.Split(new char[] { ',', ';' });
this._isMultiple = Multiple;
}

public string UsableFileTypes


{
get { return string.Join(",", _usableFileTypes); }
}

public bool Multiple


{
get { return _isMultiple; }
}

protected override ValidationResult IsValid(object filesToValidate,


ValidationContext validationContext)
{
var filemodel = validationContext.ObjectInstance as FileModel;
if(filemodel != null)
{
var files = filesToValidate as IEnumerable<HttpPostedFileBase>;
11.5 Real world esetek - Fájl le- és feltöltés 1-348

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();
}

foreach(var postedFile in files)


{
if(postedFile == null) return NoFiles();

if(postedFile.ContentLength > _maxlength)


{
return new ValidationResult(string.Format("A {0} fájl nagyobb, mint {1}KB!",
postedFile.FileName, _maxlength/1024));
}
//A kliensben nem bízunk.
if(!UsableFileTypes.Contains(postedFile.ContentType))
{
return new ValidationResult("Ilyen fájltípus nem tölthető fel!");
}
}
}
return ValidationResult.Success;
}

private ValidationResult NoFiles()


{
return new ValidationResult("Jelölj ki fájlt a feltöltéshez!");
}
}

A konstruktor három paramétert fogad:

 A feltölthető fájl maximális mérete (MaxLengthKB).


 Egy vagy több fájlt lehet egyszerre feltölteni (Multiple),
 A feltölthető fájlok típusa (FileTypes). Ezt utóbbit szétbontja vesszők, vagy pontosvesszők
szerint. A végső felhasználásában vesszőkkel elválasztott stringet készít belőle, mert az <input
accept="MIME típusok"> ezt fogadja el.

Az IsValid metódusban dől el, hogy elfogadható-e a 'Files' property tartalma. Itt van két kiértékelési
irány:

 A validationContext.ObjectInstance tartalmazza a teljes FileModel-t. A filesToValidate pedig a


feltöltött fájlok felsorolását. Ha ez a lista üres, akkor most éppen nem fájlfeltöltés történt a
FileModel-el kapcsolatban, hanem a Description mezőjének a kitöltését kell validálni. A
Description validálása nem ennek az validátornak a feladata. Tehát részéről le van tudva a
munka. Annyit azért megnéz, hogy a FileModel állapota = FileModel.UploadStatus.Temp, mert
akkor biztos a dolog.
 A másik validációs ágban a Files tartalmát kell validálni, mert fájlfeltöltés történt. Végigiterál a
feltöltött fájlokon és ellenőrzi azokat fájlméret és típus szerint. A lehetséges típust elvileg a
böngészőben megszabjuk, de a kliensben nem szabad megbízni.

Az MVC 5-ös verziójában elérhető az AcceptAttribute (DataType leszármazott), aminek a


paraméterével szintén szabályozhatjuk a feltölthető fájlok MIME típusát. Hatására bekerül a
felparaméterezett HTML 5 'accept' attribútum az <input>-ba. Hogy addig se kelljen várni míg
megjelenik az MVC következő kiadása, haladjuk tovább a saját megoldásunkkal ugyanezt az 'accept'
adta lehetőséget kihasználva.
11.5 Real world esetek - Fájl le- és feltöltés 1-349
11.5 Real world esetek - Fájl le- és feltöltés 1-350

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.

FileUpload Html helper

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:

public static MvcHtmlString FileUploadFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper,


Expression<Func<TModel, TProperty>> expression)
{
//Metaadatok megszerzése
ModelMetadata metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
//Property path
string name = ExpressionHelper.GetExpressionText(expression);
string fullName = htmlHelper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(name);

//HTML tag építése


TagBuilder tagBuilder = new TagBuilder("input");
tagBuilder.MergeAttribute("type", "file");
tagBuilder.MergeAttribute("name", fullName, true);

//Saját FileUpload attribútum megszerzése


object validationAttribute;
if(metadata.AdditionalValues.TryGetValue(FileUploadValidationAttribute.FileUploadValidationName,
out validationAttribute))
{
var fileAttribute = validationAttribute as FileUploadValidationAttribute;
if(fileAttribute != null)
{
tagBuilder.MergeAttribute("accept", fileAttribute.UsableFileTypes);
if(fileAttribute.Multiple)
tagBuilder.MergeAttribute("multiple", "multiple");
}
}
//HTML generálása
return new MvcHtmlString(tagBuilder.ToString(TagRenderMode.SelfClosing));
}

Megszerzi a ModelMetadata-t és felépíti az alapján a HTML taget. Az újdonság, hogy a


metadata.AdditionalValues-ből kiszedjük az előbb megnézett FileUploadValidation attribútumot.
Ennek a belső adatai alapján a HTML attribútumokat is hozzáragasztjuk az input-hoz. Az 'accept'
megkapja a lehetséges fájltípusokat, a 'multiple' pedig jelzi, ha lehetséges több fájl egyidejű feltöltése
a FileUploadValidation attribútum paramétere alapján. Látható, hogy mostani Html helper és a
validációs attribútum igen szoros kapcsolatban van egymással. A helper kiegészítő adatszolgáltatója,
maga az FileUploadValidation attribútum.

Eddig az AdditionalValues gyűjteményt az AdditionalMetadataAttribute-al használtuk. Akkor azt


mondtam, hogy ez az attribútum egy név-érték párt (string, object) helyez az AdditionalValues-be, amit
kinyerhetünk. Más attribútumról szó sem volt. Hogy ezt hogyan lehet elérni arra két megoldást is
mutatok.
11.5 Real world esetek - Fájl le- és feltöltés 1-351

Egy kis kitérő: Attribútumok és Html helperek kapcsolata

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.

public class ExtendedMetaDataProvider : DataAnnotationsModelMetadataProvider


{
protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes,
Type containerType, Func<object> modelAccessor,
Type modelType, string propertyName)
{
var attributeslist = attributes.ToList();

var metadata = base.CreateMetadata(attributeslist, containerType,


modelAccessor, modelType, propertyName);
var fileuploadAttr = attributeslist.OfType<FileUploadValidationAttribute>().FirstOrDefault();
if (fileuploadAttr != null)
metadata.AdditionalValues.Add(FileUploadValidationAttribute.FileUploadValidationName,
fileuploadAttr);
return metadata;
}
}

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:

ModelMetadataProviders.Current = new ExtendedMetaDataProvider();

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.)

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]


public sealed class FileUploadValidationAttribute : ValidationAttribute, IMetadataAware
{
public const string FileUploadValidationName = "FileUploadVA";

//A változatlan részek kihagyva...

public void OnMetadataCreated(ModelMetadata metadata)


{
metadata.AdditionalValues.Add(FileUploadValidationName, this);
}
}
11.5 Real world esetek - Fájl le- és feltöltés 1-352

Következzenek az actionök. A példákban az egyszerűség kedvéért a felhasználói azonosító, az aktuális


session azonosítója lesz. Mivel nem használunk adatbázist, ezért a demó céljára megfelel. Az első
actionfalatka azokat a feltöltött fájlokat listázza, amik az aktuális felhasználóé (Session-é) illetve a
státuszuk: Uploaded.

public ActionResult UploadList()


{
var newfiles = FileModel.GetList().Where(f => f.UserId == Session.SessionID
&& f.Status == FileModel.UploadStatus.Uploaded);
return View(newfiles);
}

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.

public ActionResult UploadNew()


{
return View();
}

[HttpPost]
public ActionResult UploadNew(FileModel model)
{
//Csak a 'Files' érdekes most.
if (!ModelState.IsValidField("Files"))
{
return View(model);
}

var newfiles = new List<FileModel>();


foreach (var file in model.Files)
{
if (file != null && file.ContentLength > 0) //A validációs attribútum miatt felesleges.
{
var newfile = new FileModel
{
Id = Guid.NewGuid(),
UserId = Session.SessionID, //sessionid as userid -> test only
Status = FileModel.UploadStatus.Temp,
FileName = file.FileName,
Length = file.ContentLength,
MIME = file.ContentType
};
var fileName = newfile.Id.ToString();
var path = Path.Combine(Server.MapPath("~/Upload"), fileName);
newfile.Path = path;
file.SaveAs(path);
newfiles.Add(newfile);
}
}
if (newfiles.Count > 0)
{
FileModel.AddRange(newfiles);
return RedirectToAction("UploadFill");
}
return RedirectToAction("UploadList");
}
11.5 Real world esetek - Fájl le- és feltöltés 1-353

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:

@using (Html.BeginForm("UploadNew", "FileAccess", FormMethod.Post,


new { id = "uploadForm", enctype = "multipart/form-data" }))
{
@Html.FileUploadFor(model => model.Files)<br/> @Html.ValidationMessageFor(model => model.Files)
<p>
<input type="submit" value="Feltöltés" />
</p>
}

A következő action páros felel a fájlok leírásának a kitöltetéséért.

public ActionResult UploadFill()


{
var newfiles = FileModel.GetList().Where(f => f.UserId == Session.SessionID
&& f.Status == FileModel.UploadStatus.Temp);
return View(newfiles);
}

[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>();

for (int i = 0; i < postedfileslist.Count; i++)


{
var posted = postedfileslist[i];
var fileModel = newfiles.FirstOrDefault(f => f.Id == posted.Id);
if (fileModel == null)
{
posted.Status = FileModel.UploadStatus.Deleting;
continue;
}
var descriptionkey = string.Format("[{0}].Description", i);
var descmodelstate = ModelState[descriptionkey];
if (descmodelstate.Errors.Count == 0)
{
fileModel.Description = posted.Description;
fileModel.Status = FileModel.UploadStatus.Uploaded;
//UpdateToDataBase(fileModel);
}
else
remaininvalid.Add(fileModel);

}
if (remaininvalid.Count == 0) return RedirectToAction("UploadList");

ModelState.Clear(); //1. Miért van erre szükség?


for (int i = 0; i < remaininvalid.Count; i++)
{
var descriptionkey = string.Format("[{0}].Description", i);
var ms = new ModelState();
ms.Errors.Add("A leírást meg kell adni");
ModelState.Add(descriptionkey, ms);
}
return View(remaininvalid);
}
11.5 Real world esetek - Fájl le- és feltöltés 1-354

A megjelenített, kitöltést biztosító lista:

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

Végezetül álljon itt a letöltést lebonyolító action:

public ActionResult DownloadFile(string id)


{
if (!string.IsNullOrWhiteSpace(id))
{
var model = FileModel.GetById(id);
if (model != null)
{
if (System.IO.File.Exists(model.Path))
{
return new FilePathResult(model.Path, model.MIME)
{
FileDownloadName = model.FileName
};
}
}
}
return RedirectToAction("UploadList");
}

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

11.6. Dolgozzunk egyedi View sablonokkal!

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.

A beépített scaffold template-ek megtalálhatók a Visual Studio telepítési mappájában. A VS


automatikus fájlgeneráláshoz használható sablonok az ItemTemplates mappa alá vannak szervezve. Ez
a mappa pedig a VS telepítési könyvtárában érhető el. Például VS2012 esetén a „Program Files (x86)”
mappa alól nyíló alábbi elérési úton lehetséges fellelni:

\Microsoft Visual Studio 11.0\Common7\IDE\ItemTemplates

A különböző technológákhoz tartozó almappákból a \CSharp\Web\MVC 4\CodeTemplates\ -ban


találhatóak a C# MVC generátor sablonok struktúrája:

 AddController – a kontrollerek létrehozásához használt sablonok


 AddView – a View-k sablonjai.
o AspxCSharp – aspx View template-ek gyűjteménye
o CSHTML – razor View template-ek.

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 .tt fájlok nevei megegyeznek a View generáló


dialógus ablakban megjelenő lista elemeinek
neveivel.

Ha szeretnénk módosítani a template-en, akkor ezt a mappa struktúrát le


kell másolni 62 egy új „CodeTempates” mappába az AddView mappától
kezdve. Jelen esetben, mivel C# kóddal indultunk el a könyv elején, a
CSHTML mappába kell átmásolni a Visual Studio előbb látott hosszú elérési
útjáról a kiinduló fájlokat. Példaként két fájlt másoltam át a List.tt-t és az
Edit.tt –t. Ez utóbbit átneveztem DemoEdit.tt-re.

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

ablakkal. A VS az előbbi viselkedésének eredményeként legenerál például a DemoEdit.tt alá egy


DemoEdit.cshtml fájlt is. Erre nincs szükség. Ha létrejött, akkor töröljük le.

Ahhoz, hogy ez ne történjen meg a továbbiakban, az adott


.tt fájlok tulajdonságait át kell állítani a képen látható
módon. A „Build Action” legyen „None” és a „Custom Tool”
mező tartalmát töröljük ki. Ezzel teljesen kivontuk a VS
automatizmusai alól.

Mindezek után az Add View dialógusablakban megjelennek az új scaffold template-ek.

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.

Ezek után jöhetne a .tt fájlok szerkesztése, de a VS kódformázó,


színező képességei még VS2012-ben sem működnek a T4 template
fájlokkal. Egy csúnya, szürke kóddal kéne dolgoznunk, ha nem lennének erre VS kiegészítők. A Visual
Studio Gallery 63 oldalon nagyon sok bővítést találhatunk. Szerencsére innen letölthető néhány fajta
T4 szerkesztő is a különböző VS verziókhoz. A tangible engineering GmbH ingyenes szerkesztőjét
tudom javasolni. Nyilván mások is ezt ajánlanák, ez a legpopulárisabb.

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:

<div class="editor-field"> Névtelen esemény


<# if (property.IsForeignKey) { #>
@Html.DropDownList("<#= property.Name #>", String.Empty)
<# } else { #>
@Html.EditorFor(model => model.<#= property.Name #>)
<# }#>
@Html.ValidationMessageFor(model => model.<#= property.Name #>)
</div>

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

12. MVC 5 újdonságai és változásai

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.

Rfs3339 formátumú DataTime és Timestamp értékek

Bevezetésre kerültek a Rfc3339 szabványú dátumformátumok a HTML 5 input mezők számára.


Lehetőség van beállítani a Html5DateRenderingMode.Rfc3339 enum értékkel ezt a módot a @Html.
Html5DateRenderingMode tulajdonságon keresztül. Ebben az esetben a dátumokat nem kell lokalizált
formában betölteni az input mezőbe. Ennek a string formázója : {0:yyyy-MM-ddTHH:mm:ss.fffK}

Színkiválasztás és natív Color típus

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

Bootstrap alapú projekt template

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.

Új filter héj az action körül

Az új IAuthenticationFilter interfész két metódust határoz meg. Az OnAuthentication-t ami


minden filter előtt lefut és a OnAuthenticationChallenge ami minden filter után fut le. Egyelőre, ezt
csak a Controller ősosztály valósítja meg, üres virtuális metódusokkal. Így metódusát felülbírálva
például egyedileg tudunk hitelesíteni, vagy a request számára lecserélni az IPrincipal objektumot a
HttpContext-ben és a Thread.CurrentUser-ben.
11.6 MVC 5 újdonságai és változásai - Dolgozzunk egyedi View sablonokkal! 1-359

Attribútum meghatározású routing

Lehetőségünk van action metódusonként route definíciót meghatározni attribútumokon keresztül. A


HttpGet, HttpPost és a többi HTTP method filter attribútumok kiegészültek egy konstruktorváltozattal,
amivel tudunk egyedi route definíciót meghatározni. Ezt a 5.3 fejezet elég részletesen tárgyalja.

Fájltípus megszorítások upload esetére

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.

Köszönöm a megtisztelő figyelmet.

(cc) 2013 Regius Kornél

Anda mungkin juga menyukai