Regius Kornél. (MVC 5 előzetessel) Szöveg verzió: V

Méret: px
Mutatás kezdődik a ... oldaltól:

Download "Regius Kornél. (MVC 5 előzetessel) Szöveg verzió: V"

Átírás

1 Regius Kornél (MVC 5 előzetessel) Szöveg verzió: V

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

3 FELVEZETŐ AJÁNLÓ A SZERZŐRŐL HASZNOS DOLGOK A RÖVIDÍTÉSEKRŐL, NEVEKRŐL ÉS JELEKRŐL BEVEZETÉS A TENDENCIÁK ÁTTEKINTÉSE A WEBES ALKALMAZÁSOKRÓL ÁLTALÁBAN BÖNGÉSZŐ SZERVER INTERAKCIÓ AZ ELŐZMÉNY. AZ ASP.NET WEB FORMS ASP.NET MVC PLATFORM ELŐNYEI AZ ASP.NET ÉS MVC FRAMEWORK AZ MVC KOMPONENSEI ÉS BESZERZÉSÜK A BÖNGÉSZŐKRŐL ELSŐ MEGKÖZELÍTÉS AZ MVC ARCHITEKTÚRA A MODELL A VIEW A KONTROLLER PRÓBÁLJUK KI! AZ ALKALMAZÁS FELÉPÍTÉSE ÚJ MODEL, VIEW, CONTROLLER HOZZÁADÁSA PRÓBÁLJUK KI MENTHETŐ ADATOKKAL! A PROJEKT BEÁLLÍTÁSAI A MVC KOMPONENSEINEK MŰKÖDÉSI CIKLUSA MODELL MODELL ÉS TARTALOM MODELL ÉS KÓD Viselkedéssel bővített modell Üzleti logikával bővített modell A konstruktor probléma MODELL ÉS JELLEMZŐK Megjelenés Validáció attribútumokkal MODELL ÉS TÁROLÁS. ADATPERZISZTENCIA Adatbázis séma szerinti modellek Nézet jellegű modell AZ ÉRINTHETETLEN, GENERÁLT MODELL PROBLÉMÁJA EGYÉB MODELLATTRIBÚTUMOK EGY DEMÓ MODELL A KONTROLLER ÉS KÖRNYEZETE AZ ALKALMAZÁSUNK BEÁLLÍTÁSA. A WEB.CONFIG AZ ALKALMAZÁS KIINDULÁSI PONTJA. A GLOBAL.ASAX ROUTING CONTROLLER ACTION ÉS PARAMÉTEREI AZ ACTION KIMENETE, A VIEW ADATOK ACTIONRESULT

4 ACTION KIVÁLASZTÁSA FILTEREK A VIEW A VIEW MAPPÁK A VIEW FÁJL KIVÁLASZTÁSA TARTALMA, TÍPUSOS VIEW PARTIAL VIEW A VIEW-K EGYMÁSBA ÁGYAZÁSA A VIEW NYELVEZETE Razor szintaxis Kód a View-ban Razor kulcsszavak A VIEW KONTEXTUSA BEÉPÍTETT HTML HELPEREK Nyers adatok Hivatkozás. ActionLink és RouteLink Űrlap. BeginForm Szövegbevitel. TextBox, TextArea Label és formázott megjelenítés Legördülő és normál lista Jelölők és rádióvezérlők CheckBox Editor és Display template-ek Partial és Render Partial Validációs üzenetek megjelenítése URLHELPER ASZINKRON ÜZEM, AJAX KERETRENDSZEREK TÁRHÁZA A JSON JQUERY DIÓHÉJBAN AJAX HELPEREK AJAX HELPEREK DEMÓ JSON ADATCSERE AZ MVVM KERETRENDSZEREKRŐL A MODEL BINDER EGYSZERŰ TÍPUSOK ÉS A BEÉPÍTETT LEHETŐSÉGEK FELSOROLÁSOK, LISTÁK ÉS SZÓTÁRAK BONYOLULT MODELLEK PROBLÉMÁI MÉLYEN BELÜL A BIZTONSÁG ÉS AZ ÉRTELMES ADATOK A RENDSZER BIZTONSÁGA A FRONTVONAL FELHASZNÁLÓ HITELESÍTÉS Form alapú hitelesítés Windows alapú hitelesítés OAuth, OpenID KÓDOLT AZONOSÍTÓK VALIDÁLÁS A szerver oldalon A kliens oldalon

5 REAKCIÓKÉPESSÉG, GYORSÍTÁS, MINIMALIZÁLÁS AZ OUTPUTCACHE AZ ADAT CACHE A BUNDLING REAL WORLD ESETEK TÖBBNYELVŰ ALKALMAZÁS AZ ALKALMAZÁS MODULARIZÁLÁSA. AZ AREA MOBIL NÉZETEK, VIEW VARIÁNSOK SAJÁT HTML HELPEREK, MODELL METAADATOK FÁJL LE- ÉS FELTÖLTÉS DOLGOZZUNK EGYEDI VIEW SABLONOKKAL! MVC 5 ÚJDONSÁGAI ÉS VÁLTOZÁSAI UTÓSZÓ

6 1.1 Felvezető - Ajánló 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.

7 1.2 Felvezető - A szerzőről 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-1080Z 1 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: Ez egy nagyon innovatív, közösségi fejlesztésű, moduláris, open-source MVC platform

8 1.3 Felvezető - Hasznos dolgok Hasznos dolgok A könyv legújabb verziója letölthető innen: 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: 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 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 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 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:

9 1.4 Felvezető - A rövidítésekről, nevekről és jelekről 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 JavaScriptel 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 Online ingyenes TFS. Egy verziókövető rendszer, csak ajánlani tudom.

10 2.1 Bevezetés - A tendenciák áttekintése 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 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.

11 2.2 Bevezetés - A webes alkalmazásokról általában 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: -> 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: -> c:\inetpub\kozostermekek\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: Friendly URL: 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.

12 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 Kbyte 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 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 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/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: 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,

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

14 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ól 5, 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 é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

15 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 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 után írjunk kódokat. Valójában az MVC framework képességeit a legalacsonyabb szinttől kezdve felülbírálhatjuk.

16 2.6 Bevezetés - Az ASP.NET és MVC framework 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ű 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 WebAPI 6 -ról egy könyv, amit mindenképpen ajánlok az olvasó figyelmébe, miután túljutott e könyv olvasásán 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áltozatot 7 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 oldalról érdemes letölteni. Itt található kétfajta telepítő. Az egyik a Web Platform Installer alatt

17 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: 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 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 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 FireBug 9 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

18 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 Fiddler 10. 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

19 3.1 Első megközelítés - Az MVC architektúra Első megközelítés Ez a fejezet amolyan bemelegítés, ismerkedés, bemutató célzatúnak készült alapozó. A fejezet végén már el lehet kezdeni kipróbálni az MVC lehetőségeit, sőt javasolt is egy pici alkalmazást felépíteni önállóan. A rákövetkező fejezetek alaposan nagyító alá veszik a fő komponenseket, ezek együttműködését, a lehetőségeket, elmerülve a technológiai részletekben Az MVC 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ógia 11. 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, T4 12 ). 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

20 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 actionnek 13 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. Request Home Controller példányosítás. Action kiválasztás. Modell a request alapján Controller+Action Modell a View számára View + Layout kiválasztás. View +Modell View feldolgozása Response visszaküldése MVC framework A mi M-V-C alkalmazásunk 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.

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

22 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 = " cím")] public string 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: ábra 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 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 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.

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

24 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 é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 codebehind-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 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 MvcApplication1.Models.Profile <!DOCTYPE html> <html> <head> <title>profilerazor</title> </head> <div => model.name) </div> <div => => 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 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 alfejezet mutatja be a használatát és a rejtelmeit.

25 3.4 Első megközelítés - A Kontroller 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 = " "; 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: 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 Formsban 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: 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?

26 3.5 Első megközelítés - Próbáljuk ki! 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.

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

28 3.6 Első megközelítés - Az alkalmazás felépítése 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. 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 tartalmazni ábra 5. ábra 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ó.

29 3.7 Első megközelítés - Új Model, View, Controller hozzáadása Ú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

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

31 3.7 Első megközelítés - Új Model, View, Controller hozzáadása ábra 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 stronglytyped 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. Nézzünk rá a kész View első 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.

32 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 (YSOD 14 ), 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őlactionre 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

33 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) to List", "Index") </div> Átírva: a listához", "Index") </div> Hasonló módon átírhatjuk a lista utolsó oszlopainak (Edit,Details,Delete) a feliratát (var item in Model) <tr> => item.fullname) </td>

34 3.7 Első megközelítés - Új Model, View, Controller hozzáadása 1-34 => item.address) </td> "Edit", new id = item.id "Details", new id = item.id "Delete", new id = item.id ) </td> </tr> Szeretném felhívni a figyelmet a minden sor végén ott levő paraméterekre. Ez egy anonymous objektum, ami a link elkészítésénél azt a szerepet kapja, hogy az objektum propertyjeiből URL paraméterek lesznek. Például a "Szerkesztés"/"Edit" link generálásának eredményeképpen létrejövő link markupja így néz ki: <a href="/first/edit/1">szerkesztés</a> Nem csak a kontroller és action neve kerül bele az URL-be, hanem a végére az "1" is, mert ez felel meg az id propertynek (hogy miért, azt majd hamarosan meglátjuk). Az ehhez az URL-hez tartozó action paraméterlistájában szintén ott szerepel az id: 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ó (Html.BeginForm()) <fieldset> => model.id) <div => model.fullname) </div> <div class="editor-field">

35 3.7 Első megközelítés - Új Model, View, Controller hozzáadása => => model.fullname) </div> <div => model.address) </div> <div => => 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.

36 3.8 Első megközelítés - Próbáljuk ki menthető adatokkal! 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;

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

38 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ás 15 : 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.

39 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. osztálydefiníciókat. 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 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.

40 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 => 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.cshtmlre, é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 IEnumerable<FirstMVCApp.Models.PhoneNumber> <h2>index</h2> New", "Create", new cardid = ) </p> <table> <tr> <th>

41 3.8 Első megközelítés - Próbáljuk ki menthető adatokkal! => model.number) </th> => model.cardregister.fullname) </th> <th></th> (var item in Model) <tr> => item.number) </td> => item.cardregister.fullname) </td> "Edit", "PhoneNumber", new id=item.id, "Details", "PhoneNumber", new id=item.id, "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 telefonszám", "Create", "PhoneNumber", new cardid = Model.Id, null) 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

42 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 "Edit", new id=model.id 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.

43 3.9 Első megközelítés - A projekt beállításai 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.:

44 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ó 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 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.: 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 ( 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ó.

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

46 4.1 Modell - Modell és tartalom 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á.

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

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

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

50 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 Ü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,

51 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 Architecture 17 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 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

52 4.3 Modell - Modell és jellemzők 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 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;

53 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, Address, 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. Address: [Display(Name = "Vásárló ")] [DataType(DataType. Address)] public string 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.

54 4.3 Modell - Modell és jellemzők 1-54 Ha csak megjeleníteni szeretnénk, akkor egy linket generál belőle az MVC framework (a szövegmező mellett jobbra látható kisbetűs cím). Ha viszont szerkeszteni szeretnénk, akkor böngésző megköveteli, hogy valódi címet írjunk be. Ez látható a hibás 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ó:

55 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 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 CompareAttribute CustomValidationAttribute DataTypeAttribute EnumDataTypeAttribute RangeAttribute RegularExpressionAttribute RequiredAttribute StringLengthAttribute RemoteAttribute (System.Web.Mvc) ValidationAttribute CompareAttribute (DataAnnotations) MaxLengthAttribute MinLengthAttribute MembershipPasswordAttribute DataTypeAttribute CreditCardAttribute (+Microsoft.Web.Mvc) AddressAttribute (+Microsoft.Web.Mvc) FileExtensionsAttribute (+Microsoft.Web.Mvc) 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 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:

56 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), " ", " ")] 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

57 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. Address) nem ír elő 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 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 mezővel kapcsolatban: <input class="text-box single-line" id=" " name=" " type=" " 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=" " 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. Address)] [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, AddressAttribute, 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, AddressAttribute, 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.

58 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

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

60 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(" ".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 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. )) return new ValidationResult("A címet vagy az 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.

61 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 címet meg kell adni!", new[] "Address", " " ); 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.

62 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ó ")] [DataType(DataType. Address)] //Dotnet 4.5: [ AddressAttribute] public string 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)," "," ")] //[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), = "proba@proba.hu", TotalSum = id * m, LastPurchaseDate = DateTime.Now.AddDays(-2 * id) ); return datalist[id]; private static Dictionary<int, ValidationDemoModel> datalist; public static ValidationResult ValidateFullName(string fullname)

63 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(" ".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. )) return new ValidationResult("A címet vagy az 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. (ORM 20 ). 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

64 4.4 Modell - Modell és tárolás. Adatperzisztencia Adatbázis séma szerinti modellek 11. ábra 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 odavissza 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. 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 ORMek 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

65 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 pattern 21 ) 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

66 4.4 Modell - Modell és tárolás. Adatperzisztencia Nézet jellegű modell 12. ábra 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 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;

67 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öttfogadott 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 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

68 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 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 Viewba, 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,

69 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ó 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ó ")] [DataType(DataType. Address)] //Dotnet 4.5: [ AddressAttribute] public string 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), = "proba@proba.hu", TotalSum = id * m, LastPurchaseDate = DateTime.Now.AddDays(-2 * id), PurchasesList = products, KeyPurchase = products[2] );

70 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("szam0-k1", i, DateTime.Today.Day), Quantity = rand.next(1, 1000), ProductName = string.format("01", 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.

71 5.1 A kontroller és környezete - Az alkalmazásunk beállítása. A web.config 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 ) assembly-k helyett az új 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

72 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\v \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>

73 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: " /> </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 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

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

75 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 poolban 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

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

77 5.3 A kontroller és környezete - Routing 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

78 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: de ugyanez az oldal fog megjelenni, ha a vagy ha a 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:

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

80 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 stringben 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 category 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.

81 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 vane. 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" );

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

83 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 URLbő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 /forms/one /forms/two /forms/three Fájl elérési úttal ~/AspPages/One.aspx ~/AspPages/Two.aspx ~/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 /popupmenu/product/1 /popupmenu/categories/2 /popupmenu/rates Kontroller alapú route minta /product/popupmenu/1 /categories/popupmenu/2 /rates/popupmenu

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

85 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 alapértelmezett értékét is megadtam. (defaulvalue=alapertek) az [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 new id=111) //Hivatkozás route mintára, mint 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");

86 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ú bool datetime decimal double float guid int long Karakterszám minlength(x) maxlength(x) length(l,h) Érték min(x) max(x) range(l,h) Egyedi forma alpha regex( ) A vizsgálat módja Értelmezhető-e: booleanként? (parsolható booleanra?) dátumként? decimális értékként? doubleként? floatként? guidként? egész számként? 64bites számként? A route szakasz minimális hossza karakterekben. A route szakasz maximális hossza karakterekben. A route szakasz pontos hossza karakterekben. A számként értelmezhető érték minimuma. A számként értelmezhető érték maximuma. A számként értelmezhető érték tartománya. Csak betűk lehetnek. 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 ( ) 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.

87 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ű areaban 25 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: 25 A 11.2 fejezetben lesz róla szó.

88 5.4 A kontroller és környezete - Controller 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ő

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

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

91 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

92 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

93 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

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

95 5.5 A kontroller és környezete - Action és paraméterei 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.

96 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 ViewBag.Title = "Details"; 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.

97 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 ViewBag.Title = "Details"; <h2>details</h2> <br /> <br /> <br /> 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

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

99 5.6 A kontroller és környezete - Az action kimenete, a View adatok 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 Viewbó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

100 5.6 A kontroller és környezete - Az action kimenete, a View adatok 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.

101 5.7 A kontroller és környezete - ActionResult 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;

102 5.7 A kontroller és környezete - ActionResult 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 ContentResultot küldtem volna, de application/x-javascript re beállított ContentType property tartalommal.

103 5.7 A kontroller és környezete - ActionResult Példakódok: FileContentResult, FilePathResult, FileStreamResult 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. MIME 26 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

104 5.7 A kontroller és környezete - ActionResult 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.

105 5.7 A kontroller és környezete - ActionResult 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(" 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.

106 5.7 A kontroller és környezete - ActionResult 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.

107 5.7 A kontroller és környezete - ActionResult 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.

108 5.8 A kontroller és környezete - Action kiválasztása 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. *@ további, most nem fontos kódot jelent.) A BeginForm-ról még lesz szó, a feladata egy HTML form => model.id) <div => model.fullname) </div> <div => => model.fullname) <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

109 5.8 A kontroller és környezete - Action kiválasztása 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.

110 5.9 A kontroller és környezete - Filterek 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 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 + IResultFilter Az action futása előtt és után végrehajtott közös 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.

111 5.9 A kontroller és környezete - Filterek 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

112 5.9 A kontroller és környezete - Filterek 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: akkor át fogja irányítani a böngészőt az azonos HTTPS oldalra: 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 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

113 5.9 A kontroller és környezete - Filterek 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 <script>alert(&#39;viszem a cookidat!&#39;);</script> </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 sor beiktatásával. Ott hagytam az eredetit is. (DisplayFor) <div => </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 site. Az ASP.NET-hez illeszkedő, szabályozott HTML szűrést is biztosító Microsoft Web Protection Library letölthető a oldalról.

114 5.9 A kontroller és környezete - Filterek 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 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. ActionFilterAttribute Action végrehajtás (IActionFilter) és filter konfigurálás 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 action metódusba 18:29: Enter BusinessCritical action Store(string message) a message tartalmát bejegyzi a naplóba Ez egy szöveg, amit naplóztunk ExitMethod(string method) bejegyzi, hogy kiléptünk az action metódusból 18:29: Exit 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.

115 5.9 A kontroller és környezete - Filterek 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. public virtual void OnActionExecuted(ActionExecutedContext filtercontext ) //2. public virtual void OnResultExecuting(ResultExecutingContext filtercontext) //3. public virtual void OnResultExecuted(ResultExecutedContext filtercontext) //4. //Action meghívása É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 ( ).

116 5.9 A kontroller és környezete - Filterek Í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.

117 5.9 A kontroller és környezete - Filterek 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

118 5.9 A kontroller és környezete - Filterek 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. HandleErrorAttribute Hibakezelés (IExceptionFilter) 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

119 5.9 A kontroller és környezete - Filterek 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 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ő ViewBag.Title = "HandleException"; <h2>egyedi, Action függő hibamegjelenítés</h2> Action <br /> Controller <br /> <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:

120 5.9 A kontroller és környezete - Filterek 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 FilterConfigba é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ó Viewk 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.

121 6.1 A View - A View mappák 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 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.

122 6.2 A View - A View fájl kiválasztása 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

123 6.2 A View - A View fájl kiválasztása minimalizáljuk, hogy egy picit gyorsabb legyen az MVC működése, azzal hogy ezt a keresési listát leszűkítjük 27. 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

124 6.2 A View - A View fájl kiválasztása 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.

125 6.3 A View - Tartalma, típusos View 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 => => 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.

126 6.3 A View - Tartalma, típusos View 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 => </div> Ennek a lényege ), 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 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.

127 6.4 A View - Partial View 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.) az előzővel is kombinálható. 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 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.

128 6.4 A View - Partial View 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)

129 6.4 A View - Partial View 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 = " "; ViewBag. Cime = "kukac@kukac.kc"; TempData["Tempadat"] = "Elérhető!"; return View(); public ActionResult IndexRedir() TempData["Tempadat"] = "IndexRedir Elérhető!"; return RedirectToAction("DemoTempData"); public ActionResult DemoTempData()

130 6.4 A View - Partial View 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 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> /> </div> <div class="left"> <h4>main ViewBag</h4> /> </div> <div class="left"> <h4>main ViewBag -> ViewData</h4> /> </div> <div demó", "IndexRedir") </div> <hr /> <div style="background-color: #ddd; margin-left: 20px;"> <h3>*** PartialView eleje <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>*** 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.

131 6.4 A View - Partial View A DemoPartial.cshtml kódja: <div> <em>a Partial View belső, saját tartalma:</em> </div> <div> <h5>main ViewData</h5> <br /> </div> A ChildAction.cshtml Partial View kódja (a SecondLevelChildAction.cshtml-t nem annyira fontos var parentviewdata = this.viewcontext.parentactionviewcontext.viewdata; <div> <em>a sub action belső saját tartalma</em> </div> <div class="left"> <h5>1. Child ViewData</h5> <br /> </div> <div class="left"> <h5>main ViewData</h5> /> </div> <div class="clear-fix"></div> <div style="background-color: #bbb; margin-left: 20px;"> <h3>*** 2. Child Action eleje <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 resultal tér vissza. Ezt a funkciót az IndexRedir() metódus demózza. TempData View kódja: TempData tartalma: <b>@tempdata["tempadat"]</b> <br demó újra", "DemoTempData") Ha a Tempdata demó linkre kattintunk első esetben az eredmény:

132 6.4 A View - Partial View 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 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

133 6.5 A View - A View-k egymásba ágyazása 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 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ő 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 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 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.

134 6.5 A View - A View-k egymásba ágyazása _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 required: false) </head> <body style="width: 600px; border: 1px solid #0000ff"> <h3>_layoutdemo.cshtml tartalma</h3> <div style="background-color: #eee; margin: 30px;"> required: true) </header> <div new SzovegesTartalom = "Render page demó" ) </div> 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 - 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 required: false) sort, akkor ezt úgy kell értelmezni, hogy a Layout-ot használó View-ban lehet egy footer section, ami nem kötelező footer <h3>egyben.cshtml FOOTER tartalma</h3> 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

135 6.5 A View - A View-k egymásba ágyazása 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 - 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 A megadott oldal renderelési eredményét illeszti be. Át tudunk adni egy anonymous objektumon keresztül new SzovegesTartalom = "Render page demó" ) Ezt az objektumot a renderelt View Page propertyjében kapjuk vissza. (már megint egy adatcsomag): Egyben Page /> fenti használatának egy alternatívája ez a kicsit zavarosabb 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 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.

136 6.5 A View - A View-k egymásba ágyazása Ennek kipróbálására a View-ban a Layoutot átállítottam egy köztes _LayoutDemoSub.cshtml Layout = "~/Views/Shared/_LayoutDemoSub.cshtml"; A köztes Layout fájl Layout = htmlhead <!-- A _LayoutDemoSub htmlhead beszúrt tartalma required: header <div style="background-color: #ccc; margin: 30px; border: 3px dotted #008000; padding: 5px"> <h4>a _LayoutDemoSub header beszúrt required: true) featured <div style="background-color: #ccc; margin: 30px; border: 3px dotted #008000; padding: 5px"> <h4>a _LayoutDemoSub featured beszúrt required: false) footer <div style="background-color: #ccc; margin: 30px; border: 3px dotted #008000; padding: 5px"> <h4>a _LayoutDemoSub footer beszúrt required: false) </div> <div style="background-color: #ccc; margin: 30px; border: 3px dotted #008000; padding: </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 Í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 htmlhead <!-- A _LayoutDemoSub htmlhead beszúrt tartalma 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 tartalmazzon. Remélem érthető, mindenesetre itt az eredménye:

137 6.5 A View - A View-k egymásba ágyazása 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: required: false) </footer> Ez a footer <div style="background-color: #ccc; margin: 30px; border: 3px dotted #008000; padding: 5px"> <h4>a _LayoutDemoSub footer beszúrt required: false) </div> Ez pedig az Egyben.cshtml footer <h3>egyben.cshtml FOOTER tartalma</h3> Érdekes dolog, hogy a View-ban nem számít, hogy hova rakjuk szakaszt, nincs sorrendhez kötve. Nem függ attól, hogy milyen sorrendben definiáltuk a Layout fájlban 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

138 6.5 A View - A View-k egymásba ágyazása 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 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.

139 6.6 A View - A View nyelvezete 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 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: <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. <br /> Az Oldalcím4 után kiíratásra került a böngésző neve.

140 6.6 A View - A View nyelvezete létező property</span><br /> A <span> -ra azért volt szükség, mert azt nem írhattam, 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. ) 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 aktuális URL: " + Request.Url)<br létező property<br /> Az első sor mutatja, ha 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. cím: baratom@mvc.hu<br /> Egy db /> A több sornyi C# kódot kódblokkba tudjuk tenni, amit közé kell //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 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 (1 == 1)

141 6.6 A View - A View nyelvezete //Kódblokk belüli, egysoros, nem html szöveg <p>kódblokkon belüli, html szöveg -> nem kell 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 == 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 mindenképpen ) 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 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.

142 6.6 A View - A View nyelvezete 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" = 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 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 = -> variable2-be*@ </script>

143 6.6 A View - A View nyelvezete 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 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 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 Type viewtipusa = this.gettype(); Nálam a viewtipusa ez volt: ASP._Page_Views_nezet_ViewContext_cshtml

144 6.6 A View - A View nyelvezete 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 Type viewtipusa = this.gettype(); <h2>viewcontext felfedése</h2> A View <br /> A View dll 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\v \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");

145 6.6 A View - A View nyelvezete 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 és 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 if (objektum!= null) if (objektum.prop!= null) //Lényegi kód Nincs beágyazás if (objektum == null objektum.prop == null) return; //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) 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.

146 6.6 A View - A View nyelvezete 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 Razor kulcsszavak Térjünk vissza a Razor nyelvi lehetőségeihez. A szekciók és ezek beékelésé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 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 akkor a Model property dinamikus (dynamic) típusú lesz. Amiben az a legrosszabb, hogy nincs IntelliSense támogatása. A View osztálytípusa által meghatározott generikus leszármazott lesz. Azt mondtam, 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ó System.Web.Http formában, valahol a View fájl elején. A lezáró pontosvesszőre itt sincs szükség.

147 6.6 A View - A View nyelvezete 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 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 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 a szintaxis oldalra","szintaxis", null, new = "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 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 ListItemTemplate(int index) <li>elem sorszáma: <b>@index</b></li> <h2>inline Template</h2> (int i = 1; i < 5; </ul>

148 6.6 A View - A View nyelvezete A példakód a /Views/Razor/Inside.cshtml-ben van. A működés nagyon egyszerű 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 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 Func<dynamic, object> 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 var ic = new InsideClass("Kód a View-ban"); A demó eredménye: "@ic.messagetxt"<br /> Szám /> /> <div 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"); 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. 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:

149 6.6 A View - A View nyelvezete 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 kulcsszóval tudjuk jelezni, hogy a View származási láncába beékeltünk

150 6.7 A View - A View kontextusa egy új típust. Ezek után az új típust használja a View őseként és nem a WebViewPage-et. Ezzel az í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 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 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.

151 6.8 A View - Beépített Html helperek 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 funkció metódusváltozata. Ezzel 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 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 "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 el meghatároztuk a modell típusát.

152 6.8 A View - Beépített Html helperek => => 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 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. Raw Nyers adatok. Kezdjük a legegyszerűbbel, amikor a paraméter egy string, aminek az értékét az adott helyen szeretnénk viszontlátni az elkészülendő HTML fájlban. A "féktelen nyers erő" Html.Raw metódussal tehetjük ezt meg. Azért féktelen, mert a bejövő szöveget gondolkodás és minden ellenőrzés nélkül küldi a HTML-be. Ezt is: Html.Raw( <script>alert( Lopom a cookie-dat! )</script> ) Az oldal eredménye a javascript jól ismert dialógusablaka lesz. Persze a JS kód lehetne veszélyes is. A szöveg is erre utal. Például a site-hoz tartozó cookie tartalmát továbbíthatja egy másik web applikációnak, ahol majd elcsemegéznek rajta. Lehet, hogy még pénzt is tudnak csinálni belőle a mi rovásunkra. A rébuszok után egyenesen is megmondom, hogy ezt a metódust nagyon óvatosan használjuk. Számtalan módszer van arra, olyan is amiről nem hallottunk, hogy az oldalunkra bejelentkezett felhasználótól származó nyers szövegbe, ártalmas kódot csempésszenek. Ha ezt egyszerűen kiküldjük az oldalunkra, azzal a rendszerünket és a felhasználóink adatait is veszélyeztetjük. Az MVC további Html helperei védettek az ilyen nyers adatkiküldéssel szemben. A bejövő szöveges tartalom HTML markereit (< >) átalakítják html entitásokká 29. Pl.: < < > > & & Ezzel a JS kód injektálásnak is az elejét veszik. 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

153 6.8 A View - Beépített Html helperek 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 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 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 2. oldalra, URL paraméterrel", "Hlink2", "Helper", new honnan = "Hlink", null) <a href="/helper/hlink2?honnan=hlink">a 2. oldalra, URL paraméterrel</a> Ebben a példában az URL paramétereket collection initcializer-el állítjuk be. Az eredmény azonos 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 routevalues: new honnan = "Hlink" )

154 6.8 A View - Beépített Html helperek Lehetőségünk van HTML attribútumok meghatározására is anonymous objektummal, ami egy elegáns kódformát 2. oldalra, de új ablakban", "Hlink2", null, new id = = = "_blank" ) <a class="linkek" href="/helper/hlink2" id="indexlink" target="_blank">a 2. oldalra, de ú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 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é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 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 Ű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. ( vagy /url/path/cont ). Lehet relatív, ahol

155 6.8 A View - Beépített Html helperek 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. ben 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 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").

156 6.8 A View - Beépített Html helperek (Html.BeginForm("Hform", "Helper", new id = 1, FormMethod.Post, new id = /> <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) 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

157 6.8 A View - Beépített Html helperek A form definíciója hagyományos Html (Html.BeginForm("Hinput", "Helper", new id = Model.FullName)<br />@Html.TextArea("Address", Model.Address, 2, 20, 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 Model.Address, 2, 20, null) <textarea rows="2" cols="20" id="address" name="address" /> Ott van még a Hidden 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);

158 6.8 A View - Beépített Html helperek 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 Viewn, 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 (Html.BeginForm("Hinput", "Helper", new id = => m.fullname)<br />@Html.TextAreaFor(m => m.address, 2, 20, => m.fullname, new name = "FullNameOrig", id = "hnev" ) 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.

159 6.8 A View - Beépített Html helperek Label és formázott megjelenítés Ami nem tetszik az előző példában továbbra sem, hogy 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: <br /> <br /> név") <br /> név: -0-") <br /> név: *0*") A HTML eredménye és kinézete: Label: <label for="fullname">felhasználó név</label> <br /> LabelFor: <label for="fullname">felhasználó név</label> <br /> LabelFor: <label for="fullname">teljes né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.

160 6.8 A View - Beépített Html helperek 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 id=model.id, FormMethod.Post, new id = => => 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);

161 6.8 A View - Beépített Html helperek 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 => 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.

162 6.8 A View - Beépített Html helperek [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 => 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 (Html.BeginForm("Hlist", null, new id = Model.Id, FormMethod.Post, new id = "form2" => => 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);

163 6.8 A View - Beépített Html helperek 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 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ó) => => 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 ügyfé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 => m.keypurchase) <ul style="list-style: (var pitem in Model.PurchasesList) => m.keypurchase.id, pitem.id, pitem.id == </li> </ul>

164 6.8 A View - Beépített Html helperek 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); 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.

165 6.8 A View - Beépített Html helperek 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ó ")] [DataType(DataType. Address)] public string 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.

166 6.8 A View - Beépített Html helperek 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> => item. ) </td> <td>@html.displayfor(modelitem => item.totalsum)</td> <td>@html.displayfor(modelitem => item.lastpurchasedate)</td> => item.vip) </td> "Edit", new id=item.id "Details", new id=item.id ) </td> </tr>

167 6.8 A View - Beépített Html helperek 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> </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.

168 6.8 A View - Beépített Html helperek 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 => 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 => 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.

169 6.8 A View - Beépített Html helperek 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 ("collection") Lista Lista 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" Address <a href=\"mailto: <input type=" " 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.

170 6.8 A View - Beépített Html helperek [UIHint("KEuro")] public decimal TotalSum get; set; A DisplayTemplates/KEuro.cshtml 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 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 (DisplayFor) Edit (EditorFor) 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 típusát természetesen más numerikus típusra is állíthatjuk.

171 6.8 A View - Beépített Html helperek 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.

172 6.8 A View - Beépített Html helperek DisplayForModel és EditorForModel E két helper nagyvonalú leegyszerűsítésnek is felfogható. <hr /> <div </div> dinamikus sablongenerálásból. 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 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;

173 6.8 A View - Beépített Html helperek 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-k2,-3",parentid, i, DateTime.Today.Day), Quantity = rand.next(1, 1000), ProductName = string.format("01", 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:

174 6.8 A View - Beépített Html helperek 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 MvcApplication1.Models.TemplateDemoProductModel <tr> => => model.itemno) </td> => model.productname) </td> => 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 => => model.vip) </div> <hr /> <div </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

175 6.8 A View - Beépített Html helperek 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.cshtmlbe, így is azonos eredményt i = 0; i < Model.PurchasesList.Count;i++ => 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>

176 6.8 A View - Beépített Html helperek 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 => m.purchaseslist, (int i = 0; i < Model.PurchasesList.Count; => 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 => m.purchaseslist, int i = (var item in => item, "TemplateDemoProductModel", "PurchasesList[" + i++ + "]") Partial és Render Partial Láttunk példát a Partial View-k használatára pl. 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 Html.RenderPartial("DetailList", Model.PurchasesList); Emlékeztetőül a normál Model.PurchasesList) Ugyan ez a helyzet az Action() és RenderAction Normál 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.

177 6.8 A View - Beépített Html helperek Továbbra is a TemplateDemo View mappájában levő Edit.cshtml-t kéne bővíteni most mindössze ezzel az egy 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 Viewt 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 int i = 0; <table> <tr> => model.itemno) </th> => model.productname) </th> => model.quantity) </th> (var item in => 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 => 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.

178 6.8 A View - Beépített Html helperek Validációs üzenetek megjelenítése Láttunk példát az adat érvényesség vizsgálatára a 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 MvcApplication1.Models.ValidationDemoModel <h2>validációs <br (Html.BeginForm(null, "Helper", new id = => m.fullname) <br <br />@Html.TextAreaFor(m => m.address, 4, 20, <br /> <input type="submit" /> A modell azonos propertyket és szabályokat tartalmaz, mint amit a 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)," "," ")] 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);

179 6.9 A View - UrlHelper 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 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) 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.

180 6.9 A View - UrlHelper <p> A </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 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 A teljes Url: jelenti. A paraméter nélküli az aktuális oldal (request.rawurl) relatív URL-jét 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 new action= "New",controller="Incoming") <br /> A RouteUrl new action= "New",controller="Incoming", Request.Url.Scheme) A RouteUrl 1: /complains/incoming/new A RouteUrl 2: 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

181 6.9 A View - UrlHelper 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. -> c:\www\virtualis\ -> d:\websites\amasik\ -> d:\tovabbiak\harmadikalkalmazas.5.2.1\ -> 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 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.

182 6.9 A View - UrlHelper <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 <br /> A IsLocalUrl <br /> A IsLocalUrl 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.

183 7.1 Aszinkron üzem, AJAX - Keretrendszerek tárháza 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 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

184 7.2 Aszinkron üzem, AJAX - A JSON lefedésére (DOM 30 manipulá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 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 32 adatformá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 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 jquery dióhéjban A keretrendszer oldala a 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

185 7.3 Aszinkron üzem, AJAX - jquery dióhéjban 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= 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.

186 7.3 Aszinkron üzem, AJAX - jquery dióhéjban 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

187 7.4 Aszinkron üzem, AJAX - Ajax helperek 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.cshtmlben vagy akár a View-ban is 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 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 (XHR 34 ) 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

188 7.4 Aszinkron üzem, AJAX - Ajax helperek 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 "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:

189 7.4 Aszinkron üzem, AJAX - Ajax helperek 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>

190 7.4 Aszinkron üzem, AJAX - Ajax helperek 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 Scripts 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.

191 7.4 Aszinkron üzem, AJAX - Ajax helperek 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 var ajaxoptions = new AjaxOptions HttpMethod = "Post", InsertionMode = InsertionMode.Replace, UpdateTargetId = "updatablelist", (Ajax.BeginForm("IndexListPartial", null,ajaxoptions,new id <input type="submit" value="keress"> <div 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ó:

192 7.5 Aszinkron üzem, AJAX - Ajax helperek demó <form action="/ajaxdemo/indexlistpartial" data-ajax="true" data-ajax-begin="clearerrors" data-ajaxcomplete="attachtoselectorclick" data-ajax-failure="seterror" data-ajax-loading="#betoltes" data-ajaxmethod="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.

193 7.5 Aszinkron üzem, AJAX - Ajax helperek demó 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:

194 7.5 Aszinkron üzem, AJAX - Ajax helperek demó 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 (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 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éseltűnés majd magától működni fog, nem kell kódot írni hozzá (mert már megírták). Az updatablelistbe 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.

195 7.5 Aszinkron üzem, AJAX - Ajax helperek demó A következő kód az IndexListPartial.cshtml partial View IEnumerable<MvcApplication1.Models.TemplateDemoModel> <table> <colgroup> <col class="tablecol1" /> <col class="tablecol2" /> <col class="tablecol3" /> <col class="tablecol4" /> (var item in Model) <tr data-itemid="@(item.id)"> <td> <div class="selector ui-icon ui-icon-arrow-1-e ui-button"></div> </td> => item.fullname) </td> => item.address) </td> "Details", new id = item.id, new AjaxOptions() HttpMethod = "get", InsertionMode = InsertionMode.Replace, OnBegin = "openpopupdialog", OnComplete = "closepopup", UpdateTargetId = "popupdiv" "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 selectoros 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

196 7.5 Aszinkron üzem, AJAX - Ajax helperek demó 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

197 7.5 Aszinkron üzem, AJAX - Ajax helperek demó 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.

198 7.5 Aszinkron üzem, AJAX - Ajax helperek demó Layout = null; var ajaxoptions = new AjaxOptions() HttpMethod = "post", UpdateTargetId = "editinner", Confirm = "Biztos, hogy mented?", OnBegin = "popupvalidate", OnSuccess = "successpopup", ; <div (Ajax.BeginForm("Edit", <fieldset> => model.id) (itt voltak a mező szerkesztők ) </div> <p> <input type="submit" value="mentés" /> </p> </fieldset> 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)

199 7.5 Aszinkron üzem, AJAX - Ajax helperek demó 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

200 7.5 Aszinkron üzem, AJAX - Ajax helperek demó 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.

201 7.6 Aszinkron üzem, AJAX - JSON adatcsere 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) comboboxba 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 API dokumentáció:

202 7.6 Aszinkron üzem, AJAX - JSON adatcsere 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 callbackot 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 data_completefield= "findname")

203 7.6 Aszinkron üzem, AJAX - JSON adatcsere 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

204 7.6 Aszinkron üzem, AJAX - JSON adatcsere 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"]

205 7.6 Aszinkron üzem, AJAX - JSON adatcsere 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( )\/" Ez olyan, mint egy Date objektum konstruktora, aminek a paramétere a Unix alapdátumtól 39 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ő :00:00

206 7.6 Aszinkron üzem, AJAX - JSON adatcsere 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ő adatok nyersen", "GetServerData") <br /><hr /> A válasz, benne a nem normális dátumokkal: "Message":"Szerver idő","currenttime":"\/date( )\/","eniacfinished":"\/date( )\/" 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 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

207 7.6 Aszinkron üzem, AJAX - JSON adatcsere 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( )/ formátumot olvashatóvá teszi. Természetesen szükség van erre Scripts 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ászat 40, 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.

208 7.6 Aszinkron üzem, AJAX - JSON adatcsere 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":" :17:05","date":" ","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;

209 7.6 Aszinkron üzem, AJAX - JSON adatcsere 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 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( )/ 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

210 7.6 Aszinkron üzem, AJAX - JSON adatcsere 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ó!");

211 7.6 Aszinkron üzem, AJAX - JSON adatcsere 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,

212 7.7 Aszinkron üzem, AJAX - Az MVVM keretrendszerekről néhány szóban ); 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 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.)

213 7.7 Aszinkron üzem, AJAX - Az MVVM keretrendszerekről néhány szóban 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ő

214 7.7 Aszinkron üzem, AJAX - Az MVVM keretrendszerekről néhány szóban 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.

215 7.7 A model binder - Az MVVM keretrendszerekről néhány szóban 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.

216 8.1 A model binder - Egyszerű típusok és a beépített lehetőségek 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.

217 8.1 A model binder - Egyszerű típusok és a beépített lehetőségek [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");

218 8.1 A model binder - Egyszerű típusok és a beépített lehetőségek A metódus paraméterlistájában a paraméterek nevei nem követik a modell property neveinek kisnagybetű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 Idre, 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,

219 8.1 A model binder - Egyszerű típusok és a beépített lehetőségek amit elkaphatunk a catch blokkban. A validálás alapjai a modell propertykhez kapcsolt attribútumok, ahogy az a 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 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 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 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 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

220 8.1 A model binder - Egyszerű típusok és a beépített lehetőségek 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 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 címet megváltoztató formot szeretnénk elkészíteni. A TryUpdateModel megkapja a form adatai között az címet és be is frissíti a modell 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.

221 8.1 A model binder - Egyszerű típusok és a beépített lehetőségek Ü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;

222 8.1 A model binder - Egyszerű típusok és a beépített lehetőségek 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.

223 8.2 A model binder - Felsorolások, listák és szótárak 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 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:

224 8.2 A model binder - Felsorolások, listák és szótárak 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 System.Collections.Generic.List<MvcApplication1.Models.CategoryModel> újragenerálása","recreatetree") <br/> <table style="width: 100%; background-color: #ddd;"> <tr> <th>id</th> <th>név</th> <th>létrehozva</th> (var m in => m); </table> Mivel a DisplayFor egy CategoryModel.cshtml-t vár a Views/BinderDemo/DisplayTemplates mappában, ezért erre is szükség int index = 0; int depth = (int)viewdata["depth"]; <tr style="border-bottom: 1px solid #555"> => model.id) </td> "EditTree", new id = Model.Id )

225 8.2 A model binder - Felsorolások, listák és szótárak </td> => model.createddate) </td> </tr> <tr> <td (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> (var item in => 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));

226 8.2 A model binder - Felsorolások, listák és szótárak 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=" :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 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>

227 8.2 A model binder - Felsorolások, listák és szótárak <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=" :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 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.

228 8.2 A model binder - Felsorolások, listák és szótárak 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. (var item in => 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 (index = 0; index < Model.SubCategories.Count; => 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"> => => "Di"+Model.Id) </td> <td => model.fullname) </td> <td => model.createddate) </td> </tr> <tr> <td (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> (var item in Model.SubCategories)

229 8.2 A model binder - Felsorolások, listák és szótárak </td> => item.value, "CategoryDictionaryModel", "SubCategories[" + item.key + "]", new depth = depth + 1 ) </table> </div> 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=" :57:35" /> </td>

230 8.3 A model binder - Bonyolult modellek problémái 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.

231 8.3 A model binder - Bonyolult modellek problémái á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.

232 8.4 A model binder - Mélyen belül 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

233 8.4 A model binder - Mélyen belül 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 bindert. 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));

234 8.4 A model binder - Mélyen belül 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 IModelBindert 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();

235 8.4 A model binder - Mélyen belül 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)

236 8.4 A model binder - Mélyen belül 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 (Html.BeginForm("EditCategModelBinder","BinderDemo", new Id="99999", FormMethod.Post))

237 8.4 A model binder - Mélyen belül 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 (Html.BeginForm("EditCategModelBinder", "BinderDemo", new Id = "99999", WillNeverValid ="Hát ez honnan jött?", 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 (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.

238 8.4 A model binder - Mélyen belül 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"/>

239 8.4 A model binder - Mélyen belül 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.

240 9.1 A biztonság és az értelmes adatok - A rendszer biztonsága 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 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 é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.

241 9.2 A biztonság és az értelmes adatok - A frontvonal 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ényekedve, jó- vagy rosszindulata szerint. Igyekszem a bemutatást az MVC belső feldolgozási folyamatának a sorrendjében megtenni 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 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 érkezike. 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

242 9.2 A biztonság és az értelmes adatok - A frontvonal 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 (Html.BeginForm("AntiForgeryServed", <input type="submit" value=" Ment " /> És az actionre illeszteni az ellenőrző attribútum párját: [HttpPost] [ValidateAntiForgeryToken] 42

243 9.2 A biztonság és az értelmes adatok - A frontvonal 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();

244 9.2 A biztonság és az értelmes adatok - A frontvonal 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:

245 9.3 A biztonság és az értelmes adatok - Felhasználó hitelesítés 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/ 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 lefelkapcsoló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ő MVCnek elég, ha ezt felhasználja. Emiatt akinek ismerős az ASP.NET hitelesítési rendszere, annak az MVC

246 9.3 A biztonság és az értelmes adatok - Felhasználó hitelesítés 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 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 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.

247 9.3 A biztonság és az értelmes adatok - Felhasználó hitelesítés 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:

248 9.3 A biztonság és az értelmes adatok - Felhasználó hitelesítés dZL4XjLYkd_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 vane 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.

249 9.3 A biztonság és az értelmes adatok - Felhasználó hitelesítés 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

250 9.3 A biztonság és az értelmes adatok - Felhasználó hitelesítés 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 (WebMatrixbó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 RequiresUnique 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 GetUserNameBy (string ); 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.

251 9.3 A biztonság és az értelmes adatok - Felhasználó hitelesítés 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 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- MvcApplication ;Integrated Security=SSPI;AttachDBFilename= DataDirectory \aspnet- MvcApplication 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

252 9.3 A biztonság és az értelmes adatok - Felhasználó hitelesítés nem biztosítja a WebSecurity. Ha történetesen ebben telefonszámot, 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 -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

253 9.3 A biztonság és az értelmes adatok - Felhasználó hitelesítés megerősítést váró levéllel. A jóváhagyási ben 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

254 9.3 A biztonság és az értelmes adatok - Felhasználó hitelesítés 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 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.

255 9.3 A biztonság és az értelmes adatok - Felhasználó hitelesítés 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 rolenak, 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 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, cím, telefonszám, szervezeti egység mind rendelkezésre áll az ADban). 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.

256 9.3 A biztonság és az értelmes adatok - Felhasználó hitelesítés 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) :

257 9.3 A biztonság és az értelmes adatok - Felhasználó hitelesítés 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;

258 9.3 A biztonság és az értelmes adatok - Felhasználó hitelesítés 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 cím és a jelszó megadása után még egy biztonsági megerősítést kér:

259 9.3 A biztonság és az értelmes adatok - Felhasználó hitelesítés 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 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 cím. Azonban nem olyan barátságos és spam veszélyes is, hogy kiírjuk a felhasználó címét. Próbáljunk ezen javítani egy kicsit.

260 9.3 A biztonság és az értelmes adatok - Felhasználó hitelesítés 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[" "] és a result.username. Naná, hogy mindkettő az 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 OnBeforeSendingAuthenticationRequestben ö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: 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];

261 9.4 A biztonság és az értelmes adatok - Kódolt azonosítók // 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 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

262 9.4 A biztonság és az értelmes adatok - Kódolt azonosítók 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

263 9.4 A biztonság és az értelmes adatok - Kódolt azonosítók 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 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 odavissza adatcserébe.

264 9.4 A biztonság és az értelmes adatok - Kódolt azonosítók 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 JavaScriptSerializerrel 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) )

265 9.4 A biztonság és az értelmes adatok - Kódolt azonosítók 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.

266 9.4 A biztonság és az értelmes adatok - Kódolt azonosítók 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

267 9.4 A biztonság és az értelmes adatok - Kódolt azonosítók 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 SecurityModel <h3>saját kódolt hidden html (Html.BeginForm("EncodedHidden", Model.FullName) <input type="submit" value=" Ment " />

268 9.4 A biztonság és az értelmes adatok - Kódolt azonosítók 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 (Html.BeginForm("EncodedInternalHidden", /> <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.

269 9.4 A biztonság és az értelmes adatok - Kódolt azonosítók 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:

270 9.4 A biztonság és az értelmes adatok - Kódolt azonosítók 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: 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.

271 9.4 A biztonság és az értelmes adatok - Kódolt azonosítók 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ítani 45. 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 oldal, ahova az Id elkódolva érkezik", "EncodedUrl","Security", Model.HId) <h3>elkódolt Id és további query string az 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.

272 9.4 A biztonság és az értelmes adatok - Kódolt azonosítók 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ében 46. É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

273 9.5 A biztonság és az értelmes adatok - Validálás 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.

274 9.5 A biztonság és az értelmes adatok - Validálás 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 mező és ebben az érték. Mondjuk osztalyvezeto@cegnev.com, akkor ez az 1. kategória szerint érvényes az cím. Van és nem számmal kezdődik, 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 címmel ilyen entitás, akkor az 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 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 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 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.

275 9.5 A biztonság és az értelmes adatok - Validálás 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ó ")] [DataType(DataType. Address)] public string 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 (Html.BeginForm(null, "Validations", new id = Model.Id, => => m.fullname)<br <br => m.address)<br => m.address, 4, 20, null)<br <br /> <input type="submit" /> Emiatt valami probléma lehet majd, ha validálni akarjuk a [Required] propertyket.

276 9.5 A biztonság és az értelmes adatok - Validálás 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.

277 9.5 A biztonság és az értelmes adatok - Validálás 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), " ", " ")] [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.

278 9.5 A biztonság és az értelmes adatok - Validálás 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 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 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.

279 9.5 A biztonság és az értelmes adatok - Validálás "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.

280 9.5 A biztonság és az értelmes adatok - Validálás [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), = "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

281 9.5 A biztonság és az értelmes adatok - Validálás 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), " ", " ")] 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?

282 9.5 A biztonság és az értelmes adatok - Validálás Az alaposztály propertyjén nézzen ki így a definíció: [Range(typeof(DateTime), " ", " ")] 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.

283 9.5 A biztonság és az értelmes adatok - Validálás 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

284 9.5 A biztonság és az értelmes adatok - Validálás 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

285 9.5 A biztonság és az értelmes adatok - Validálás 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 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ó 48 jelentő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 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

286 9.5 A biztonság és az értelmes adatok - Validálás 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.

287 9.5 A biztonság és az értelmes adatok - Validálás 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 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

288 9.5 A biztonság és az értelmes adatok - Validálás 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=" :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";

289 9.5 A biztonság és az értelmes adatok - Validálás 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 <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 és razor funkciók működésére. A sorrendet nem hanem 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 <script type="text/javascript"> //Adapter: $.validator.unobtrusive.adapters.addsingleval("daterelative", "reldate"); //Validátor: $.validator.addmethod("daterelative", function (value, element, relativedate) return true; ); </script>

290 9.5 A biztonság és az értelmes adatok - Validálás 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 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.

291 9.5 A biztonság és az értelmes adatok - Validálás 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ökhátrányok.

292 9.5 A biztonság és az értelmes adatok - Validálás 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át 49, 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: 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. 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)

293 9.5 A biztonság és az értelmes adatok - Validálás 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ő

294 9.5 A biztonság és az értelmes adatok - Validálás 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.

295 9.5 A biztonság és az értelmes adatok - Validálás 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 (Validation). Ezt a kódblokkot lehet implementálni statikusan is: közös validációs kódblokk A MyRemote attribútum össze van kapcsolva a controller, action paramétereivel a ValidationActionnel, ez végezteti majd el a kliens oldali validációt a MyRemoteAttribute.Validation metódussal.

296 9.5 A biztonság és az értelmes adatok - Validálás 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 ).

297 9.5 Reakcióképesség, gyorsítás, minimalizálás. - Validálás 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 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ó 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 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 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

298 10.1 Reakcióképesség, gyorsítás, minimalizálás. - Az OutputCache 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 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));

299 10.1 Reakcióképesség, gyorsítás, minimalizálás. - Az OutputCache 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 stringnek. (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ő HH:mm:ss.fff") </div> <! további kódok, ld példa --> new id = 1 ) </tr> new id = 2 ) </tr> 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.

300 10.1 Reakcióképesség, gyorsítás, minimalizálás. - Az OutputCache 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

301 10.1 Reakcióképesség, gyorsítás, minimalizálás. - Az OutputCache 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: Accept Accept-Encoding Accept-Language Cache-Control Connection Host User-Agent text/html,application/xhtml+xml,application/xml;q=0.9,* /*;q=0.8 gzip, deflate en-us,en;q=0.5 max-age=0 keep-alive localhost:18005 Mozilla/5.0 (Windows NT 6.2; WOW64; rv:21.0) Gecko/ 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.

302 10.1 Reakcióképesség, gyorsítás, minimalizálás. - Az OutputCache 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")]

303 10.1 Reakcióképesség, gyorsítás, minimalizálás. - Az OutputCache 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 Enum érték Any Client Downstream Viselkedés Az összeállított HTML tartalom bárhol tárolható. A böngészőben a proxy szerveren és a web szerveren is. Ez az alapértelmezett működés. A tartalom csak a böngészőben kerül gyorítótárazásra. A cache-elt tartalom tárolható a böngészőben és a köztes proxy szerveren is. Cache-Control public private public 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 Date Expires Last-Modified public, max-age=120 Tue, 11 Jun :12:07 GMT Tue, 11 Jun :14:07 GMT Tue, 11 Jun :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 :24:37 GMT Expires -1 Pragma no-cache

304 10.1 Reakcióképesség, gyorsítás, minimalizálás. - Az OutputCache 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:

305 10.1 Reakcióképesség, gyorsítás, minimalizálás. - Az OutputCache 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ő Viewval 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: 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 cacheelé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.

306 10.2 Reakcióképesség, gyorsítás, minimalizálás. - Az adat Cache 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 AJAXos, részleges oldalfrissítéshez is jól illeszkedik. Példának okáért a "Középső rész" szekció, a maga 0 cache idejével, teljes egészében AJAX frissítésű lehet. A fánkos cache-elésnek van egy kikötése, nem mintha sok értelme lenne, de az OutputCache-t és az Authorize attribútumot nem használhatjuk egy child actionön egyszerre. Bővítések Amennyiben nem elégszünk meg az ASP.NET output Cache működésével, a lehetőség nyitott, hogy egyedi cache providert használjuk a 4.0-ás verzió megjelenése óta. Mivel ez a téma példakódok nélkül elég nehezen bemutatható, az értelmes példakódok pedig elég terjedelmesek lennének, ezért egy letölthető demót is mellékelő cikket tudnék ajánlani, ami egy fájl alapú output cache provider implementálását mutatja be: ASP.NET 4.0: Writing custom output cache providers Az 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ékei 50 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ő.

307 10.2 Reakcióképesség, gyorsítás, minimalizálás. - Az adat Cache 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.

308 10.2 Reakcióképesség, gyorsítás, minimalizálás. - Az adat Cache 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)

309 10.2 Reakcióképesség, gyorsítás, minimalizálás. - Az adat Cache 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).

310 10.2 Reakcióképesség, gyorsítás, minimalizálás. - Az adat Cache [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-practise 51 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 ASP.NET Caching: Techniques and Best Practices

311 10.2 Reakcióképesség, gyorsítás, minimalizálás. - Az adat Cache 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.

312 10.3 Reakcióképesség, gyorsítás, minimalizálás. - A Bundling 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 50msra, 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ás 52, 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

313 10.3 Reakcióképesség, gyorsítás, minimalizálás. - A Bundling 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 A fenti sor a _ Layout.cshtml-ben 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. 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.

314 10.3 Reakcióképesség, gyorsítás, minimalizálás. - A Bundling 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 js"></script> <script src="/scripts/jquery-ui 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):

315 10.3 Reakcióképesség, gyorsítás, minimalizálás. - A Bundling 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 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 soksok 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 URLrő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.

316 10.3 Reakcióképesség, gyorsítás, minimalizálás. - A Bundling 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", " "~/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 Selenium 54 -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

317 10.3 Reakcióképesség, gyorsítás, minimalizálás. - A Bundling 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 #4D926F; #header h2 most nem razor kódot vezet be. Ennek megvan az MVC-ben használható megvalósítása, ami a 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 CssMinifynek) 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);

318 10.3 Reakcióképesség, gyorsítás, minimalizálás. - A Bundling A böngészőhöz egy minifikált CSS szövegtartalom fog megérkezni a behelyettesített szín konstansokkal: #headercolor:#4d926fh2color:#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>

319 11.1 Real world esetek - Többnyelvű alkalmazás 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 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.

320 11.1 Real world esetek - Többnyelvű alkalmazás 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, stb UILabels.en.resx, UILabels.en-GB.resx, UILabels.en-US.resx 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

321 11.1 Real world esetek - Többnyelvű alkalmazás 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 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.

322 11.1 Real world esetek - Többnyelvű alkalmazás 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.

323 11.1 Real world esetek - Többnyelvű alkalmazás 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: (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)

324 11.1 Real world esetek - Többnyelvű alkalmazás 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",

325 11.1 Real world esetek - Többnyelvű alkalmazás "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>@( ( ).ToString("") )</td> <td></td> </tr> <tr> <td>nev</td> <td>@mvcapplication1.resources.uilabels.fullnamelabel</td> <td>@html.displayfor(m => m.fullname)</td>

326 11.1 Real world esetek - Többnyelvű alkalmazás </tr> <tr> <td>cim</td> <td>@html.labelfor(m => m.address)</td> <td>@html.displayfor(m => m.address)</td> </tr>

327 11.2 Real world esetek - Az alkalmazás modularizálása. Az Area 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 kontrollerszolgá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.

328 11.2 Real world esetek - Az alkalmazás modularizálása. Az Area 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 );

329 11.2 Real world esetek - Az alkalmazás modularizálása. Az Area 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 dlljé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:

330 11.2 Real world esetek - Az alkalmazás modularizálása. Az Area 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" );

331 11.2 Real world esetek - Az alkalmazás modularizálása. Az Area 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 projekt Home", "Index", "Home", new area = "", null) <br> 2 area Home", "Index", "Home") <br> 3 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.

332 11.2 Real world esetek - Az alkalmazás modularizálása. Az Area 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 projekt Home", "Index", "Home") <br> 2 area Home", "Index", "Home", new area = "Admin", null) <br> 3 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 Layout = "~/Areas/Admin/Views/Shared/_Layout.cshtml";

333 11.3 Real world esetek - Mobil nézetek, View variánsok 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.: / 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.

334 11.3 Real world esetek - Mobil nézetek, View variánsok 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 ( oldal), mikor böngészőtípusfüggővé tettük az action elérését. Majd a speciális output cache esetében ( 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\v \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 headerben 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-useragent-text") metódusváltozattal tudjuk ezt megtenni.

335 11.3 Real world esetek - Mobil nézetek, View variánsok 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 MobiOne Studio 57 Opera Mobile Emulator 58 A komplett Windows Phone SDK részeként érhető el. Ingyenes. iphone, ipad, Nexsus, Android precíz emulációk. 15 napos próbaverzió. 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 <h3>desktop partial View</h3> IndexPartial.Mobile <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 <br />Nem szükséges így hivatkozni:<br

336 11.3 Real world esetek - Mobil nézetek, View variánsok 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 (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.

337 11.3 Real world esetek - Mobil nézetek, View variánsok 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 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.

338 11.4 Real world esetek - Saját Html helperek, modell metaadatok 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 => 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);

339 11.4 Real world esetek - Saját Html helperek, modell metaadatok 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:

340 11.4 Real world esetek - Saját Html helperek, modell metaadatok 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 (ViewData.ModelMetadata.AdditionalValues.ContainsKey("modell metainfo"))

341 11.4 Real world esetek - Saját Html helperek, modell metaadatok 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 expressiont 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));

342 11.4 Real world esetek - Saját Html helperek, modell metaadatok //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>

343 11.5 Real world esetek - Fájl le- és feltöltés Abban az esetben, ha ajánlottan saját névteret használunk, akkor vagy feltüntetjük a View elején al, vagy új elemet adunk a fenti web.config 'namespaces' definíciólistához 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 x 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 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: 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

344 11.5 Real world esetek - Fájl le- és feltöltés 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 (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

345 11.5 Real world esetek - Fájl le- és feltöltés 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 inputokat. 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 (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>

346 11.5 Real world esetek - Fájl le- és feltöltés 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")]

347 11.5 Real world esetek - Fájl le- és feltöltés 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>;

348 11.5 Real world esetek - Fájl le- és feltöltés 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 1KB!", 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.

349 11.5 Real world esetek - Fájl le- és feltöltés 1-349

350 11.5 Real world esetek - Fájl le- és feltöltés 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.

351 11.5 Real world esetek - Fájl le- és feltöltés 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);

352 11.5 Real world esetek - Fájl le- és feltöltés 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");

353 11.5 Real world esetek - Fájl le- és feltöltés 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 (Html.BeginForm("UploadNew", "FileAccess", FormMethod.Post, new id = "uploadform", enctype = "multipart/form-data" => => 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);

354 11.5 Real world esetek - Fájl le- és feltöltés 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 MvcApplication1.Controllers.Files.FileModel <tr> => => model.id) </td> => => model.description) </td> => model.length) </td> => 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

355 11.5 Real world esetek - Fájl le- és feltöltés 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 AJAXosan 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ék

356 11.6 Real world esetek - Dolgozzunk egyedi View sablonokkal! 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.

357 11.6 Real world esetek - Dolgozzunk egyedi View sablonokkal! 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) property.name #>", String.Empty) <# else => model.<#= property.name #>) <# => 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

Webes alkalmazások fejlesztése. Bevezetés az ASP.NET MVC 5 keretrendszerbe

Webes alkalmazások fejlesztése. Bevezetés az ASP.NET MVC 5 keretrendszerbe Webes alkalmazások fejlesztése Bevezetés az ASP.NET MVC 5 keretrendszerbe ASP.NET MVC Framework 2009-ben jelent meg az első verziója, azóta folyamatosan fejlesztik Nyílt forráskódú Microsoft technológia

Részletesebben

1. fejezet Bevezetés a web programozásába (Balássy György munkája)... 11 Az internet működése... 11

1. fejezet Bevezetés a web programozásába (Balássy György munkája)... 11 Az internet működése... 11 Tartalomjegyzék 1. fejezet Bevezetés a web programozásába (Balássy György munkája)... 11 Az internet működése... 11 Géptől gépig... 11 Számok a gépeknek... 13 Nevek az embereknek... 14 Programok egymás

Részletesebben

Webes alkalmazások fejlesztése

Webes alkalmazások fejlesztése Webes alkalmazások fejlesztése 3. gyakorlat Authentikáció, adatok feltöltése Szabó Tamás (sztrabi@inf.elte.hu) - sztrabi.web.elte.hu Authentikáció Manapság már elvárás, hogy a felhasználó regisztrálni

Részletesebben

Web-fejlesztés NGM_IN002_1

Web-fejlesztés NGM_IN002_1 Web-fejlesztés NGM_IN002_1 Rich Internet Applications RIA Vékony-kliens generált (statikus) HTML megjelenítése szerver oldali feldolgozással szinkron oldal megjelenítéssel RIA desktop alkalmazások funkcionalitása

Részletesebben

Miért érdemes váltani, mikor ezeket más szoftverek is tudják?

Miért érdemes váltani, mikor ezeket más szoftverek is tudják? Néhány hónapja elhatároztam, hogy elkezdek megismerkedni az Eclipse varázslatos világával. A projektet régóta figyelemmel kísértem, de idő hiányában nem tudtam komolyabban kipróbálni. Plusz a sok előre

Részletesebben

Google App Engine az Oktatásban 1.0. ügyvezető MattaKis Consulting http://www.mattakis.com

Google App Engine az Oktatásban 1.0. ügyvezető MattaKis Consulting http://www.mattakis.com Google App Engine az Oktatásban Kis 1.0 Gergely ügyvezető MattaKis Consulting http://www.mattakis.com Bemutatkozás 1998-2002 között LME aktivista 2004-2007 Siemens PSE mobiltelefon szoftverfejlesztés,

Részletesebben

Webes alkalmazások fejlesztése. 9. előadás Bevezetés az ASP.NET MVC keretrendszerbe

Webes alkalmazások fejlesztése. 9. előadás Bevezetés az ASP.NET MVC keretrendszerbe Webes alkalmazások fejlesztése 9. előadás Bevezetés az ASP.NET MVC keretrendszerbe ASP.NET MVC Framework 2009-ben jelent meg az első verziója, azóta folyamatosan fejlesztik Nyílt forráskódú Microsoft technológia

Részletesebben

Gyakorlati vizsgatevékenység A

Gyakorlati vizsgatevékenység A Gyakorlati vizsgatevékenység A Szakképesítés azonosító száma, megnevezése: 481 04 0000 00 00 Web-programozó Vizsgarészhez rendelt követelménymodul azonosítója, megnevezése: 1189-06 Web-alkalmazás fejlesztés

Részletesebben

Flash és PHP kommunikáció. Web Konferencia 2007 Ferencz Tamás Jasmin Media Group Kft

Flash és PHP kommunikáció. Web Konferencia 2007 Ferencz Tamás Jasmin Media Group Kft Flash és PHP kommunikáció Web Konferencia 2007 Ferencz Tamás Jasmin Media Group Kft A lehetőségek FlashVars External Interface Loadvars XML SOAP Socket AMF AMFphp PHPObject Flash Vars Flash verziótól függetlenül

Részletesebben

Webshop készítése ASP.NET 3.5 ben I.

Webshop készítése ASP.NET 3.5 ben I. Webshop készítése ASP.NET 3.5 ben I. - Portál kialakíása - Mesteroldal létrehozása - Témák létrehozása Site létrehozása 1. File / New Web site 2. A Template k közül válasszuk az ASP.NEt et, nyelvnek (Language)

Részletesebben

Internet programozása. 1. előadás

Internet programozása. 1. előadás Internet programozása 1. előadás Áttekintés 1. Mi a PHP? 2. A PHP fejlődése 3. A PHP 4 újdonságai 4. Miért pont PHP? 5. A programfejlesztés eszközei 1. Mi a PHP? Egy makrókészlet volt, amely személyes

Részletesebben

Bevezetés Működési elv AJAX keretrendszerek AJAX

Bevezetés Működési elv AJAX keretrendszerek AJAX AJAX Áttekintés Bevezetés Működési elv AJAX-ot támogató keretrendszerek Áttekintés Bevezetés Működési elv AJAX-ot támogató keretrendszerek Áttekintés Bevezetés Működési elv AJAX-ot támogató keretrendszerek

Részletesebben

Gyakorlati vizsgatevékenység B

Gyakorlati vizsgatevékenység B Gyakorlati vizsgatevékenység Szakképesítés azonosító száma, megnevezése: 481 04 0000 00 00 Web-programozó Vizsgarészhez rendelt követelménymodul azonosítója, megnevezése: 1189-06 Web-alkalmazás fejlesztés

Részletesebben

WWW Kliens-szerver Alapfogalmak Technológiák Terv. Web programozás 1 / 31

WWW Kliens-szerver Alapfogalmak Technológiák Terv. Web programozás 1 / 31 Web programozás 2011 2012 1 / 31 Áttekintés Mi a web? / A web rövid története Kliens szerver architektúra Néhány alapfogalom Kliens- illetve szerver oldali technológiák áttekintése Miről lesz szó... (kurzus/labor/vizsga)

Részletesebben

Tájékoztató. Használható segédeszköz: -

Tájékoztató. Használható segédeszköz: - A 12/2013. (III. 29.) NFM rendelet szakmai és vizsgakövetelménye alapján. Szakképesítés, azonosítószáma és megnevezése 54 481 06 Informatikai rendszerüzemeltető Tájékoztató A vizsgázó az első lapra írja

Részletesebben

George Shepherd. 1. A webes alkalmazások alapjai 1

George Shepherd. 1. A webes alkalmazások alapjai 1 George Shepherd Köszönetnyilvánítás Bevezetés Az ASP.NET 2.0 fejlesztése A klasszikus ASP ASP.NET 1.0 és 1.1 ASP.NET 2.0 Néhány szó a.net-futtatórendszerről A könyv használatáról Kinek szól a könyv? A

Részletesebben

Webes alkalmazások fejlesztése 2. előadás. Webfejlesztés MVC architektúrában (ASP.NET) Webfejlesztés MVC architektúrában Fejlesztés ASP.

Webes alkalmazások fejlesztése 2. előadás. Webfejlesztés MVC architektúrában (ASP.NET) Webfejlesztés MVC architektúrában Fejlesztés ASP. Eötvös Loránd Tudományegyetem Informatikai Kar Webes alkalmazások fejlesztése 2. előadás Webfejlesztés MVC architektúrában (ASP.NET) 2015 Giachetta Roberto groberto@inf.elte.hu http://people.inf.elte.hu/groberto

Részletesebben

CMS-en túli webes megoldások

CMS-en túli webes megoldások CMS-en túli webes megoldások Rigó Tamás (rigo.tamas@p-foto.hu) Miről is lesz szó? Miért is, mikor is Felmerült ötletek, igények Minta megoldások Így írunk mi Hol kezdjem Joomla! Framework Támogatás, segítségkérés

Részletesebben

VALUTAISMERTETŐ FUNKCIÓNÁLIS SPECIFIKÁCIÓ

VALUTAISMERTETŐ FUNKCIÓNÁLIS SPECIFIKÁCIÓ VALUTAISMERTETŐ FUNKCIÓNÁLIS SPECIFIKÁCIÓ Tartalomjegyzék. Áttekintés Rendszerkövetelmények A szoftver funkciói Interfészek Képernyőképek Főképernyő Általános ismertető Valuta nézet Bankjegy nézet Csekkek

Részletesebben

TUDNIVALÓK A WEB-FEJLESZTÉS I. KURZUSRÓL

TUDNIVALÓK A WEB-FEJLESZTÉS I. KURZUSRÓL TUDNIVALÓK A WEB-FEJLESZTÉS I. KURZUSRÓL http://bit.ly/a1lhps Abonyi-Tóth Andor Egyetemi tanársegéd 1117, Budapest XI. kerület, Pázmány Péter sétány 1/C, 2.404 Tel: (1) 372-2500/8466 http://abonyita.inf.elte.hu

Részletesebben

MVC. Model View Controller

MVC. Model View Controller MVC Model View Controller Szoftver fejlesztés régen Console-based alkalmazások Pure HTML weboldalak Assembly, C Tipikusan kevés fejlesztő (Johm Carmack Wolfenstein, Doom, Quake..) Szűkös erőforrások optimális

Részletesebben

Programozási alapismeretek 4.

Programozási alapismeretek 4. Programozási alapismeretek 4. Obejktum-Orientált Programozás Kis Balázs Bevezetés I. Az OO programozási szemlélet, egy merőben más szemlélet, az összes előző szemlélettel (strukturális, moduláris, stb.)

Részletesebben

Magyar Nemzeti Bank - Elektronikus Rendszer Hitelesített Adatok Fogadásához ERA. Elektronikus aláírás - felhasználói dokumentáció

Magyar Nemzeti Bank - Elektronikus Rendszer Hitelesített Adatok Fogadásához ERA. Elektronikus aláírás - felhasználói dokumentáció ERA Elektronikus aláírás - felhasználói dokumentáció Tartalomjegyzék 1. Bevezető... 3 1.1. Általános információk... 3 2. DesktopSign... 3 2.1. Általános információk... 3 2.2. Telepítés... 3 3. MNBSubscriber...

Részletesebben

JAVA webes alkalmazások

JAVA webes alkalmazások JAVA webes alkalmazások Java Enterprise Edition a JEE-t egy specifikáció definiálja, ami de facto szabványnak tekinthető, egy ennek megfelelő Java EE alkalmazásszerver kezeli a telepített komponensek tranzakcióit,

Részletesebben

A CAPICOM ActiveX komponens telepítésének és használatának leírása Windows 7 operációs rendszer és Internet Explorer 9 verziójú böngésző esetén

A CAPICOM ActiveX komponens telepítésének és használatának leírása Windows 7 operációs rendszer és Internet Explorer 9 verziójú böngésző esetén A CAPICOM ActiveX komponens telepítésének és használatának leírása Windows 7 operációs rendszer és Internet Explorer 9 verziójú böngésző esetén Tartalomjegyzék 1. Az Internet Explorer 9 megfelelősségének

Részletesebben

Miért ASP.NET? Egyszerű webes alkalmazás fejlesztése. Történet ASP ASP.NET. Működés. Készítette: Simon Nándor

Miért ASP.NET? Egyszerű webes alkalmazás fejlesztése. Történet ASP ASP.NET. Működés. Készítette: Simon Nándor Miért ASP.NET? Egyszerű webes alkalmazás fejlesztése Készítette: Simon Nándor Integrált fejlesztő környezet Egységes (vizuális) fejlesztési lehetőségek Bőséges segítség (help) Hibakeresési, nyomkövetési

Részletesebben

Bevezető. Servlet alapgondolatok

Bevezető. Servlet alapgondolatok A Java servlet technológia Fabók Zsolt Ficsor Lajos Általános Informatikai Tanszék Miskolci Egyetem Utolsó módosítás: 2008. 03. 06. Servlet Bevezető Igény a dinamikus WEB tartalmakra Előzmény: CGI Sokáig

Részletesebben

Tájékoztató. Használható segédeszköz: -

Tájékoztató. Használható segédeszköz: - A 35/2016. (VIII. 31.) NFM rendelet szakmai és vizsgakövetelménye alapján. Szakképesítés, azonosító száma és megnevezése 54 481 06 Informatikai rendszerüzemeltető Tájékoztató A vizsgázó az első lapra írja

Részletesebben

Digitális aláíró program telepítése az ERA rendszeren

Digitális aláíró program telepítése az ERA rendszeren Digitális aláíró program telepítése az ERA rendszeren Az ERA felületen a digitális aláírásokat a Ponte webes digitális aláíró program (Ponte WDAP) segítségével lehet létrehozni, amely egy ActiveX alapú,

Részletesebben

COMET webalkalmazás fejlesztés. Tóth Ádám Jasmin Media Group

COMET webalkalmazás fejlesztés. Tóth Ádám Jasmin Media Group COMET webalkalmazás fejlesztés Tóth Ádám Jasmin Media Group Az előadás tartalmából Alapproblémák, fundamentális kérdések Az eseményvezérelt architektúra alapjai HTTP-streaming megoldások AJAX Polling COMET

Részletesebben

Tisztelt Felhasználó!

Tisztelt Felhasználó! Tisztelt Felhasználó! Az alábbiakban az NB termékek 3D modelljeinek generálása, használata kerül bemutatásra. A webes felület használatához regisztráció nem szükséges! Tartalomjegyzék Belépés... 2 Szükséges

Részletesebben

Mobilizálódó OSZK. A nemzeti könyvtár mobileszközöket célzó fejlesztései az elmúlt időszakban. Garamvölgyi László. Networkshop, 2013.

Mobilizálódó OSZK. A nemzeti könyvtár mobileszközöket célzó fejlesztései az elmúlt időszakban. Garamvölgyi László. Networkshop, 2013. ORSZÁGOS SZÉCHÉNYI KÖNYVTÁR WEBTARTALOM KOORDINÁCIÓS OSZTÁLY Mobilizálódó OSZK A nemzeti könyvtár mobileszközöket célzó fejlesztései az elmúlt időszakban Garamvölgyi László Networkshop, 2013. Okostelefonok

Részletesebben

Ustream.tv Bepillantás egy közösségi élővideo site működésébe

Ustream.tv Bepillantás egy közösségi élővideo site működésébe Ustream.tv Bepillantás egy közösségi élővideo site működésébe Tolmács Márk Pillantás bele... Facebook-clean, YouTube simple......so you get the drill Mivel kell szembenéznünk... Web front-end 30 000 kérés

Részletesebben

Webes alkalmazások fejlesztése Bevezetés. Célkitűzés, tematika, követelmények. A.NET Core keretrendszer

Webes alkalmazások fejlesztése Bevezetés. Célkitűzés, tematika, követelmények. A.NET Core keretrendszer Eötvös Loránd Tudományegyetem Informatikai Kar Webes alkalmazások fejlesztése Bevezetés Célkitűzés, tematika, követelmények A.NET Core keretrendszer Cserép Máté mcserep@inf.elte.hu http://mcserep.web.elte.hu

Részletesebben

Telenor Webiroda. Kezdő lépések

Telenor Webiroda. Kezdő lépések Telenor Webiroda Kezdő lépések Virtuális Tárgyaló Tartalom 1. Bevezetés...2 2. A szolgáltatás elérése és a kliensprogram letöltése...3 3. A kliensprogram telepítése...6 4. A Virtuális Tárgyaló használatba

Részletesebben

Dropbox - online fájltárolás és megosztás

Dropbox - online fájltárolás és megosztás Dropbox - online fájltárolás és megosztás web: https://www.dropbox.com A Dropbox egy felhő-alapú fájltároló és megosztó eszköz, melynek lényege, hogy a különböző fájlokat nem egy konkrét számítógéphez

Részletesebben

2009.11.20. Weboldalkészítés sablonok segítségével Nyitrai Erika. Miről lesz szó? WEBOLDALKÉSZÍTÉS SABLONOK SEGÍTSÉGÉVEL. Saját honlapot szeretnék

2009.11.20. Weboldalkészítés sablonok segítségével Nyitrai Erika. Miről lesz szó? WEBOLDALKÉSZÍTÉS SABLONOK SEGÍTSÉGÉVEL. Saját honlapot szeretnék Miről lesz szó? ELTE IK Algoritmusok és Alkalmazásaik Tanszék WEBOLDALKÉSZÍTÉS SABLONOK SEGÍTSÉGÉVEL Mit tehetek, ha szeretnék egy saját honlapot vagy blogot? Mik a főbb problémák? Milyen megoldások születhetnek?

Részletesebben

Webes alkalmazások fejlesztése Bevezetés. Célkitűzés, tematika, követelmények. A.NET Core keretrendszer

Webes alkalmazások fejlesztése Bevezetés. Célkitűzés, tematika, követelmények. A.NET Core keretrendszer Eötvös Loránd Tudományegyetem Informatikai Kar Webes alkalmazások fejlesztése Célkitűzés, tematika, követelmények A.NET Core keretrendszer Cserép Máté mcserep@inf.elte.hu http://mcserep.web.elte.hu Célkitűzés

Részletesebben

Földmérési és Távérzékelési Intézet

Földmérési és Távérzékelési Intézet Ta p a s z ta l a to k é s g ya ko r l a t i m e g o l d á s o k a W M S s zo l gá l tatá s b a n Földmérési és Távérzékelési Intézet 2011.03.13. WMS Szolgáltatások célja A technikai fejlődéshez igazodva

Részletesebben

Testreszabott alkalmazások fejlesztése Notes és Quickr környezetben

Testreszabott alkalmazások fejlesztése Notes és Quickr környezetben Testreszabott alkalmazások fejlesztése Notes és Quickr környezetben Szabó János Lotus Brand Manager IBM Magyarországi Kft. 1 Testreszabott alkalmazások fejlesztése Lotus Notes és Quickr környezetben 2

Részletesebben

Információs technológiák 2. Gy: CSS, JS alapok

Információs technológiák 2. Gy: CSS, JS alapok Információs technológiák 2. Gy: CSS, JS alapok 1/69 B ITv: MAN 2017.10.01 Ismétlés Van egy Web nevű mappánk, ebben vannak az eddig elkészített weboldalak (htm, html) képek (jpg, png). Logikai felépítés

Részletesebben

Előszó... 13. 1. A Windows alkalmazásfejlesztés rövid története... 15. A Windows életútja... 15 A Windows 8 paradigmaváltása... 16

Előszó... 13. 1. A Windows alkalmazásfejlesztés rövid története... 15. A Windows életútja... 15 A Windows 8 paradigmaváltása... 16 Előszó... 13 1. A Windows alkalmazásfejlesztés rövid története... 15 A Windows életútja... 15 A Windows 8 paradigmaváltása... 16 A Microsoft megteszi az első lépéseket a fogyasztók felé... 17 A Windows

Részletesebben

SDL Trados szervermegoldások. Szekeres Csaba SDL Trados partner szekeres.csaba@m-prospect.hu M-Prospect Kft.

SDL Trados szervermegoldások. Szekeres Csaba SDL Trados partner szekeres.csaba@m-prospect.hu M-Prospect Kft. SDL Trados szervermegoldások Szekeres Csaba SDL Trados partner szekeres.csaba@m-prospect.hu M-Prospect Kft. Fókuszban A fájlalapú fordítási memória korlátai SDL TM Server 2009 A fájlalapú terminológiai

Részletesebben

Szia Ferikém! Készítek neked egy leírást mert bánt, hogy nem sikerült személyesen megoldani a youtube problémát. Bízom benne, hogy segít majd.

Szia Ferikém! Készítek neked egy leírást mert bánt, hogy nem sikerült személyesen megoldani a youtube problémát. Bízom benne, hogy segít majd. Szia Ferikém! Készítek neked egy leírást mert bánt, hogy nem sikerült személyesen megoldani a youtube problémát. Bízom benne, hogy segít majd. Első lépés: Töltsd le a programot innen: http://download.vessoft.com/files/fyds/freeyoutubedownoad.exe

Részletesebben

MVC Java EE Java EE Kliensek JavaBeanek Java EE komponensek Web-alkalmazások Fejlesztői környezet. Java Web technológiák

MVC Java EE Java EE Kliensek JavaBeanek Java EE komponensek Web-alkalmazások Fejlesztői környezet. Java Web technológiák Java Web technológiák Bevezetés Áttekintés Model View Controller (MVC) elv Java EE Java alapú Web alkalmazások Áttekintés Model View Controller (MVC) elv Java EE Java alapú Web alkalmazások Áttekintés

Részletesebben

Digitális aláíró program telepítése az ERA rendszeren

Digitális aláíró program telepítése az ERA rendszeren Digitális aláíró program telepítése az ERA rendszeren Az ERA felületen a digitális aláírásokat a Ponte webes digitális aláíró program (Ponte WDAP) segítségével lehet létrehozni, amely egy ActiveX alapú,

Részletesebben

Kedvenc Ingyenes editorok avagy milyen a programozó jobbkeze? PSPAD editor DEVPHP IDE

Kedvenc Ingyenes editorok avagy milyen a programozó jobbkeze? PSPAD editor DEVPHP IDE Kedvenc Ingyenes editorok avagy milyen a programozó jobbkeze? Az Interneten nagyon sok fizetős szoftver gyakorlatilag sz sem ér, ezért mindenkinek azt javaslom mielőtt még gyors költekezésbe kezdene nézzen

Részletesebben

VirtueMart bővítmény letölthető termékek eladásához

VirtueMart bővítmény letölthető termékek eladásához Kézikönyv a VirtueMart letölthető termékek bővítményhez. Ez a bővítmény lehetővé teszi a digitális termékek fizetős, vagy ingyenes, vagy regisztráláshoz kötött letöltését. Pld.: Szoftverek, e-könyvek,

Részletesebben

Iman 3.0 szoftverdokumentáció

Iman 3.0 szoftverdokumentáció Melléklet: Az iman3 program előzetes leírása. Iman 3.0 szoftverdokumentáció Tartalomjegyzék 1. Az Iman rendszer...2 1.1. Modulok...2 1.2. Modulok részletes leírása...2 1.2.1. Iman.exe...2 1.2.2. Interpreter.dll...3

Részletesebben

Microsoft SQL Server telepítése

Microsoft SQL Server telepítése Microsoft SQL Server telepítése Az SQL Server a Microsoft adatbázis kiszolgáló megoldása Windows operációs rendszerekre. Az SQL Server 1.0 verziója 1989-ben jelent meg, amelyet tizenegy további verzió

Részletesebben

MÉRY Android Alkalmazás

MÉRY Android Alkalmazás MÉRY Android Alkalmazás Felhasználói kézikönyv Di-Care Zrt. Utolsó módosítás: 2014.06.12 Oldal: 1 / 7 Tartalomjegyzék 1. Bevezetés 3 1.1. MÉRY Android alkalmazás 3 1.2. A MÉRY Android alkalmazás funkciói

Részletesebben

Web programoz as 2009 2010

Web programoz as 2009 2010 Web programozás 2009 2010 Áttekintés A web rövid története Kliens szerver architektúra Néhány alapfogalom Kliens- illetve szerver oldali technológiák áttekintése Áttekintés: miről lesz szó (kurzus/labor/vizsga)

Részletesebben

Telepítési útmutató a SMART Response 2009 szoftverhez

Telepítési útmutató a SMART Response 2009 szoftverhez Telepítési útmutató a SMART Response 2009 szoftverhez Tisztelt Felhasználó! Ezt a dokumentációt abból a célból hoztuk létre, hogy segítse Önt a telepítés során. Kövesse az alábbi lépéseket, és a telepítés

Részletesebben

A Zotero hivatkozáskezelő program bemutatása. Mátyás Melinda

A Zotero hivatkozáskezelő program bemutatása. Mátyás Melinda A Zotero hivatkozáskezelő program bemutatása Mátyás Melinda Mire használható a Zotero? A Zotero egy ingyenes hivatkozáskezelő program Különböző internetes oldalakról, adatbázisokból tudjuk kinyerni a megjelenített

Részletesebben

JavaScript Web AppBuilder használata

JavaScript Web AppBuilder használata JavaScript Web AppBuilder használata Kiss András Esri Magyarország Kft. 2015. október 8. Az ArcGIS Platform lehetővé teszi a Web GIS-t Térinformatika elérése bárhonnan Desktop Web Eszköz Egyszerű Egységes

Részletesebben

Selling Platform Telepítési útmutató Gyakori hibák és megoldások

Selling Platform Telepítési útmutató Gyakori hibák és megoldások Selling Platform Telepítési útmutató Gyakori hibák és megoldások 265ced1609a17cf1a5979880a2ad364653895ae8 Index _ Amadeus szoftvertelepítő 3 _ Rendszerkövetelmények 3 Támogatott operációs rendszerek 3

Részletesebben

Általános e-mail fiók beállítási útmutató

Általános e-mail fiók beállítási útmutató Általános e-mail fiók beállítási útmutató Ennek az összeállításnak az a célja, hogy segítséget nyújtsunk azon Ügyfeleink számára, akik az IntroWeb Kft. által nyújtott e-mail szolgáltatáshoz be szeretnék

Részletesebben

Tisztelt Ügyfelünk. Az internet beállítások kinézete. Itt a Speciális fülre kell kattintani.

Tisztelt Ügyfelünk. Az internet beállítások kinézete. Itt a Speciális fülre kell kattintani. Tisztelt Ügyfelünk. Október 8-án reggeltől a KÖKIR kapcsolat nem mindenhol működik. Jelenleg a hatóság is vizsgálja a jelenséget. A hibaüzenet amit KÖKIR adatbázistól visszakapunk a következő: Az alapprobléma

Részletesebben

Útmutató az OKM 2007 FIT-jelentés telepítéséhez

Útmutató az OKM 2007 FIT-jelentés telepítéséhez Útmutató az OKM 2007 FIT-jelentés telepítéséhez 1. OKM 2007 FIT-JELENTÉS ASZTALI HÁTTÉRALKALMAZÁS telepítése 2. Adobe Acrobat Reader telepítése 3. Adobe SVG Viewer plugin telepítése Internet Explorerhez

Részletesebben

Telepítési Kézikönyv

Telepítési Kézikönyv Intelligens Dokumentum Kezelő Rendszer Telepítési Kézikönyv 1/15. oldal Dokumentum áttekintés Dokumentum címe: doknet telepítési kézikönyv Dokumentum besorolása: szoftver telepítési leírás Projektszám:

Részletesebben

Telepítési útmutató a SMART Notebook 10.6 oktatói szoftverhez

Telepítési útmutató a SMART Notebook 10.6 oktatói szoftverhez Telepítési útmutató a SMART Notebook 10.6 oktatói szoftverhez Tisztelt Felhasználó! Ezt a dokumentációt abból a célból hoztuk létre, hogy segítse Önt a telepítés során. Kövesse az alábbi lépéseket, és

Részletesebben

BaBér bérügyviteli rendszer telepítési segédlete 2011. év

BaBér bérügyviteli rendszer telepítési segédlete 2011. év BaBér bérügyviteli rendszer telepítési segédlete 2011. év Ajánlott konfiguráció A program hardverigénye: Konfiguráció: 2800 MHz processzor 512 Mbyte memória (RAM) / Szerver gépen 1G memória (RAM) Lézernyomtató

Részletesebben

Java Server Pages - JSP. Web Technológiák. Java Server Pages - JSP. JSP lapok életciklusa

Java Server Pages - JSP. Web Technológiák. Java Server Pages - JSP. JSP lapok életciklusa Web Technológiák Java Server Pages - JSP Répási Tibor egyetemi tanársegéd Miskolc Egyetem Infomatikai és Villamosmérnöki Tanszékcsoport (IVM) Általános Informatikai Tanszék Iroda: Inf.Int. 108. Tel: 2101

Részletesebben

Thermo1 Graph. Felhasználói segédlet

Thermo1 Graph. Felhasználói segédlet Thermo1 Graph Felhasználói segédlet A Thermo Graph program a GIPEN Thermo eszközök Windows operációs rendszeren működő grafikus monitorozó programja. A program a telepítést követően azonnal használható.

Részletesebben

minic studio Melinda Steel Weboldal kivitelezési árajánlat 2013.03.01.

minic studio Melinda Steel Weboldal kivitelezési árajánlat 2013.03.01. minic studio Melinda Steel Weboldal kivitelezési árajánlat 2013.03.01. Weboldal 1. Előkészítés 1.1. Anyaggyűjtés 1.2. Kutatás 2. Tervezés 3. Kivitelezés 3.1. Drótváz 3.2. Grafikus tervezés 3.3. Programozás

Részletesebben

Az FMH weboldal megnyitásakor megjelenő angol nyelvű üzenetek eltüntetése

Az FMH weboldal megnyitásakor megjelenő angol nyelvű üzenetek eltüntetése Az FMH weboldal megnyitásakor megjelenő angol nyelvű üzenetek eltüntetése A Java kliensprogram telepítése, és megfelelő beállítása szükséges az FMH weblap megfelelő működéséhez. Ha nincs telepítve vagy

Részletesebben

QGIS gyakorló. --tulajdonságok--stílus fül--széthúzás a terjedelemre).

QGIS gyakorló. --tulajdonságok--stílus fül--széthúzás a terjedelemre). QGIS gyakorló Cím: A Contour-, a Point sampling tool és a Terrain profile pluginek használata. DEM letöltése: http://www.box.net/shared/1v7zq33leymq1ye64yro A következő gyakorlatban szintvonalakat fogunk

Részletesebben

OpenCL alapú eszközök verifikációja és validációja a gyakorlatban

OpenCL alapú eszközök verifikációja és validációja a gyakorlatban OpenCL alapú eszközök verifikációja és validációja a gyakorlatban Fekete Tamás 2015. December 3. Szoftver verifikáció és validáció tantárgy Áttekintés Miért és mennyire fontos a megfelelő validáció és

Részletesebben

Webkezdő. A modul célja

Webkezdő. A modul célja Webkezdő A modul célja Az ECDL Webkezdő modulvizsga követelménye (Syllabus 1.5), hogy a jelölt tisztában legyen a Webszerkesztés fogalmával, és képes legyen egy weboldalt létrehozni. A jelöltnek értenie

Részletesebben

Microsoft Office PowerPoint 2007 fájlműveletei

Microsoft Office PowerPoint 2007 fájlműveletei Microsoft Office PowerPoint 2007 fájlműveletei Program megnyitása Indítsuk el valamelyik tanult módszerrel a 2007-es verziót. Figyeljük meg, hogy most más felületet kapunk, mint az eddigi megszokott Office

Részletesebben

Kameleon Light Bootloader használati útmutató

Kameleon Light Bootloader használati útmutató Kameleon Light Bootloader használati útmutató 2017. Verzió 1.0 1 Tartalom jegyzék 2 1. Bootloader bevezető: A Kameleon System-hez egy összetett bootloader tartozik, amely lehetővé teszi, hogy a termékcsalád

Részletesebben

A leírás bemutatja hogy mint minden másra, Favicon készítésre is alkalmas az ingyenes Gimp rajzolóprogram.

A leírás bemutatja hogy mint minden másra, Favicon készítésre is alkalmas az ingyenes Gimp rajzolóprogram. A leírás bemutatja hogy mint minden másra, Favicon készítésre is alkalmas az ingyenes Gimp rajzolóprogram. 1, Készítsünk egy 160 160-as új képet. Azért kell ekkora, hogy kényelmesen elférjünk benne, majd

Részletesebben

ÁNYK53. Az Általános nyomtatványkitöltő (ÁNYK), a személyi jövedelemadó (SZJA) bevallás és kitöltési útmutató együttes telepítése

ÁNYK53. Az Általános nyomtatványkitöltő (ÁNYK), a személyi jövedelemadó (SZJA) bevallás és kitöltési útmutató együttes telepítése ÁNYK53 Az Általános nyomtatványkitöltő (ÁNYK), a személyi jövedelemadó (SZJA) bevallás és kitöltési útmutató együttes telepítése Az ÁNYK53 egy keretprogram, ami a személyi jövedelemadó bevallás (SZJA,

Részletesebben

Java és web programozás

Java és web programozás Budapesti Műszaki Egyetem 2015. 04. 08. 9. Előadás Kivétel kezelés a kivétel (exception) egy esemény, mely futás közben megbontja a program normális futási folyamatát például kivétel dobódik amikor 0-val

Részletesebben

Google Drive szinkronizálása asztali géppel Linux rendszeren

Google Drive szinkronizálása asztali géppel Linux rendszeren Google Drive szinkronizálása asztali géppel Linux rendszeren Ha valamilyen Ubuntu disztribúciót használsz, akkor nincs nehéz dolgod a telepítést illetően, hiszen egyszerűen PPA tárolóban is elérhető. Az

Részletesebben

Tel.: 06-30/218-3519 E-mail: probert@petorobert.com. Közösségi megosztás előnyei és alkalmazása

Tel.: 06-30/218-3519 E-mail: probert@petorobert.com. Közösségi megosztás előnyei és alkalmazása Tel.: 06-30/218-3519 E-mail: probert@petorobert.com Közösségi megosztás előnyei és alkalmazása Tartalomjegyzék KÖZÖSSÉGI MEGOSZTÁS - 2 - MIÉRT HASZNOS? - 2 - A JÓ SHARE GOMB ISMERTETŐ JELEI - 3 - MEGOSZTÁSI

Részletesebben

AJAX Framework építés. Nagy Attila Gábor Wildom Kft. nagya@wildom.com

AJAX Framework építés. Nagy Attila Gábor Wildom Kft. nagya@wildom.com AJAX Framework építés Wildom Kft. nagya@wildom.com Mi az AJAX? Asynchronous JavaScript and XML Ennél azért kicsit több: Koncepció váltás a felhasználói interface tervezésben Standard kompatibilis HTML!

Részletesebben

Telepítési útmutató a SMART Notebook 10 SP1 szoftverhez

Telepítési útmutató a SMART Notebook 10 SP1 szoftverhez Tisztelt Felhasználó! Telepítési útmutató a SMART Notebook 10 SP1 szoftverhez Ezt a dokumentációt abból a célból hoztuk létre, hogy segítse Önt a telepítés során. Kövesse az alábbi lépéseket, és a telepítés

Részletesebben

Flex: csak rugalmasan!

Flex: csak rugalmasan! Flex: csak rugalmasan! Kiss-Tóth Marcell http://kiss-toth.hu marcell@kiss-toth.hu Magyarországi Web Konferencia 2006 2006. március 18. tartalom bevezető Adobe Flex alternatív technológiák bevezető az Internetnek

Részletesebben

Adatbázis rendszerek. dr. Siki Zoltán

Adatbázis rendszerek. dr. Siki Zoltán Adatbázis rendszerek I. dr. Siki Zoltán Adatbázis fogalma adatok valamely célszerűen rendezett, szisztéma szerinti tárolása Az informatika elterjedése előtt is számos adatbázis létezett pl. Vállalati személyzeti

Részletesebben

WIN-TAX programrendszer frissítése

WIN-TAX programrendszer frissítése WIN-TAX programrendszer frissítése A WIN-TAX programrendszert a verzió érvényességének lejártakor illetve jelentősebb változás esetén (pl.: elkészült fejlesztések, munkahelyi hálózati szinkronitás miatt)

Részletesebben

Autóipari beágyazott rendszerek Dr. Balogh, András

Autóipari beágyazott rendszerek Dr. Balogh, András Autóipari beágyazott rendszerek Dr. Balogh, András Autóipari beágyazott rendszerek Dr. Balogh, András Publication date 2013 Szerzői jog 2013 Dr. Balogh András Szerzői jog 2013 Dunaújvárosi Főiskola Kivonat

Részletesebben

Petőfi Irodalmi Múzeum. megújuló rendszere technológiaváltás

Petőfi Irodalmi Múzeum. megújuló rendszere technológiaváltás Petőfi Irodalmi Múzeum A Digitális Irodalmi Akadémia megújuló rendszere technológiaváltás II. Partnerek, feladatok Petőfi Irodalmi Múzeum Megrendelő, szakmai vezetés, kontroll Konzorcium MTA SZTAKI Internet

Részletesebben

A NetBeans IDE Ubuntu Linux operációs rendszeren

A NetBeans IDE Ubuntu Linux operációs rendszeren A NetBeans IDE Ubuntu Linux operációs rendszeren Készítette: Török Viktor (Kapitány) E-mail: kapitany@lidercfeny.hu 1/10 A NetBeans IDE Linux operációs rendszeren Bevezető A NetBeans IDE egy Java-ban írt,

Részletesebben

Hiba bejelentés azonnal a helyszínről elvégezhető. Egységes bejelentési forma jön létre Követhető, dokumentált folyamat. Regisztráció.

Hiba bejelentés azonnal a helyszínről elvégezhető. Egységes bejelentési forma jön létre Követhető, dokumentált folyamat. Regisztráció. Ingyenes Mobil helpdesk megoldás A Mobil helpdesk egy olyan androidos felületen futó hibabejelentő, amelynek néhány alapbeállítását megadva saját mobil hibabejelentő rendszere lehet, vagy partnereinek

Részletesebben

Email Marketing szolgáltatás tájékoztató

Email Marketing szolgáltatás tájékoztató Email Marketing szolgáltatás tájékoztató RENDESWEB Kft. Érvényes: 2013.03.01-től visszavonásig +3 20 A RENDES (273 337) Adószám: 12397202-2-42 Cégjegyzékszám: 01-09-7079 1. Minőség Nálunk legmagasabb prioritást

Részletesebben

Telepítési útmutató a Solid Edge ST7-es verziójához Solid Edge

Telepítési útmutató a Solid Edge ST7-es verziójához Solid Edge Telepítési útmutató a Solid Edge ST7-es verziójához Solid Edge Tartalomjegyzék Bevezetés 2 Szükséges hardver és szoftver konfiguráció 3 Testreszabások lementése előző Solid Edge verzióból 4 Előző Solid

Részletesebben

Alkalmazások fejlesztése A D O K U M E N T Á C I Ó F E L É P Í T É S E

Alkalmazások fejlesztése A D O K U M E N T Á C I Ó F E L É P Í T É S E Alkalmazások fejlesztése A D O K U M E N T Á C I Ó F E L É P Í T É S E Követelmény A beadandó dokumentációját a Keszthelyi Zsolt honlapján található pdf alapján kell elkészíteni http://people.inf.elte.hu/keszthelyi/alkalmazasok_fejlesztese

Részletesebben

FTP Az FTP jelentése: File Transfer Protocol. Ennek a segítségével lehet távoli szerverek és a saját gépünk között nagyobb állományokat mozgatni. Ugyanez a módszer alkalmas arra, hogy a kari web-szerveren

Részletesebben

Országos Területrendezési Terv térképi mel ékleteinek WMS szolgáltatással történő elérése, Quantum GIS program alkalmazásával Útmutató 2010.

Országos Területrendezési Terv térképi mel ékleteinek WMS szolgáltatással történő elérése, Quantum GIS program alkalmazásával Útmutató 2010. Országos Területrendezési Terv térképi mellékleteinek WMS szolgáltatással történő elérése, Quantum GIS program alkalmazásával Útmutató 2010. május 1. BEVEZETÉS Az útmutató célja az Országos Területrendezési

Részletesebben

Az alábbi kód egy JSON objektumot definiál, amiből az adtokat JavaScript segítségével a weboldal tartalmába ágyazzuk.

Az alábbi kód egy JSON objektumot definiál, amiből az adtokat JavaScript segítségével a weboldal tartalmába ágyazzuk. JSON tutorial Készítette: Cyber Zero Web: www.cyberzero.tk E-mail: cyberzero@freemail.hu Msn: cyberzero@mailpont.hu Skype: cyberzero_cz Fb: https://www.facebook.com/cyberzero.cz BEVEZETÉS: A JSON (JavaScript

Részletesebben

Kezdő lépések Microsoft Outlook

Kezdő lépések Microsoft Outlook Kezdő lépések Microsoft Outlook A Central Europe On-Demand Zrt. által, a Telenor Magyarország Zrt. részére nyújtott szolgáltatások rövid kezelési útmutatója 1 Tartalom Áttekintés... 3 MAPI mailbox konfiguráció

Részletesebben

Rendszergazda Debrecenben

Rendszergazda Debrecenben LEVELEZŐKLIENS BEÁLLÍTÁSA A levelezés kényelmesen kliensprogramokkal is elérhető, és használható. Ezen útmutató beállítási segítséget nyújt, két konkrét klienssel bemutatva képernyőképekkel. Természetesen

Részletesebben

Smarty AJAX. Miért jó ez? Ha utálsz gépelni, akkor tudod. Milyen műveletet tudunk elvégezni velük:

Smarty AJAX. Miért jó ez? Ha utálsz gépelni, akkor tudod. Milyen műveletet tudunk elvégezni velük: Smarty AJAX Smarty sablonrendszer fegyverzetét (Funkcióit) igyekszik kiegészíteni, néhány alap AJAX metódussal, amivel a megjelenést, kényelmet vagy a funkcionalitást növelhetjük. A Smarty Ajax függvényeknek

Részletesebben

Hogyan készítsünk Colorbox-os képgalériát Drupal 7-ben?

Hogyan készítsünk Colorbox-os képgalériát Drupal 7-ben? Hogyan készítsünk Colorbox-os képgalériát Drupal 7-ben? (Jó segítség: http://www.youtube.com/watch?v=gstnfznz3hg) I. Telepteni kell az alábbi három dolgot 1. A Colorbox modult (https://www.drupal.org/project/colorbox)

Részletesebben

A B rész az Informatikai szakmai angol nyelv modul témaköreit tartalmazza.

A B rész az Informatikai szakmai angol nyelv modul témaköreit tartalmazza. A vizsgafeladat ismertetése: A szóbeli központilag összeállított vizsga kérdései a 4. Szakmai követelmények fejezetben megadott témaköröket tartalmazza. Amennyiben a tétel kidolgozásához segédeszköz szükséges,

Részletesebben

Tartalomjegyzék. Bevezetés. 1. A.NET 3.5-keretrendszer 1. A korszerű alkalmazások felépítésének kihívásai... 2

Tartalomjegyzék. Bevezetés. 1. A.NET 3.5-keretrendszer 1. A korszerű alkalmazások felépítésének kihívásai... 2 Bevezetés xv Mitől tartozik egy platform a következő generációhoz?... xvi Mennyire jelentős az egyre újabb.net-változatok közötti különbség?... xviii Mit jelentett a Windows Vista megjelenése a Microsoft.NET

Részletesebben

Az alábbiakban szeretnénk segítséget nyújtani Önnek a CIB Internet Bankból történő nyomtatáshoz szükséges böngésző beállítások végrehajtásában.

Az alábbiakban szeretnénk segítséget nyújtani Önnek a CIB Internet Bankból történő nyomtatáshoz szükséges böngésző beállítások végrehajtásában. Tisztelt Ügyfelünk! Az alábbiakban szeretnénk segítséget nyújtani Önnek a CIB Internet Bankból történő nyomtatáshoz szükséges böngésző beállítások végrehajtásában. A CIB Internet Bankból történő nyomtatás

Részletesebben

Tanúsítvány igénylése sportegyesületek számára

Tanúsítvány igénylése sportegyesületek számára Microsec Számítástechnikai Fejlesztő zrt. Tanúsítvány igénylése sportegyesületek számára Felhasználói útmutató ver. 1.0 Budapest, 2017. január 04. 1 A Microsigner telepítő letöltése A telepítés megkezdéséhez

Részletesebben

PHP-MySQL. Adatbázisok gyakorlat

PHP-MySQL. Adatbázisok gyakorlat PHP-MySQL Adatbázisok gyakorlat Weboldalak és adatbázisok Az eddigiek során megismertük, hogyan lehet a PHP segítségével dinamikus weblapokat készíteni. A dinamikus weboldalak az esetek többségében valamilyen

Részletesebben