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.
AJÁNLÓ ................................................................................................................................................. 1-6 A SZERZŐRŐL .......................................................................................................................................... 1-7 HASZNOS DOLGOK ................................................................................................................................... 1-8 A RÖVIDÍTÉSEKRŐL, NEVEKRŐL ÉS JELEKRŐL................................................................................................... 1-9 BEVEZETÉS ................................................................................................................................... 2-10
2.1. 2.2. 2.3. 2.4. 2.5. 2.6. 2.1. 2.2. 3.
A TENDENCIÁK ÁTTEKINTÉSE .................................................................................................................... 2-10 A WEBES ALKALMAZÁSOKRÓL ÁLTALÁBAN ................................................................................................... 2-11 BÖNGÉSZŐ – SZERVER INTERAKCIÓ ............................................................................................................ 2-12 AZ ELŐZMÉNY. AZ ASP.NET WEB FORMS ................................................................................................. 2-13 ASP.NET MVC PLATFORM ELŐNYEI ......................................................................................................... 2-15 AZ ASP.NET ÉS MVC FRAMEWORK ......................................................................................................... 2-16 AZ MVC KOMPONENSEI ÉS BESZERZÉSÜK. .................................................................................................. 2-16 A BÖNGÉSZŐKRŐL.................................................................................................................................. 2-17 ELSŐ MEGKÖZELÍTÉS .................................................................................................................... 3-19
AZ MVC ARCHITEKTÚRA ......................................................................................................................... 3-19 A MODELL ........................................................................................................................................... 3-21 A VIEW ............................................................................................................................................... 3-23 A KONTROLLER ..................................................................................................................................... 3-25 PRÓBÁLJUK KI! ...................................................................................................................................... 3-26 AZ ALKALMAZÁS FELÉPÍTÉSE ..................................................................................................................... 3-28 ÚJ MODEL, VIEW, CONTROLLER HOZZÁADÁSA ............................................................................................ 3-29 PRÓBÁLJUK KI MENTHETŐ ADATOKKAL! ...................................................................................................... 3-36 A PROJEKT BEÁLLÍTÁSAI ........................................................................................................................... 3-43 A MVC KOMPONENSEINEK MŰKÖDÉSI CIKLUSA ........................................................................................... 3-44 MODELL ....................................................................................................................................... 4-46
MODELL ÉS TARTALOM ........................................................................................................................... 4-46 MODELL ÉS KÓD .................................................................................................................................... 4-48 Viselkedéssel bővített modell ...................................................................................................... 4-48 Üzleti logikával bővített modell.................................................................................................... 4-50 A konstruktor probléma ............................................................................................................... 4-51 MODELL ÉS JELLEMZŐK ........................................................................................................................... 4-52 Megjelenés ................................................................................................................................... 4-52 Validáció attribútumokkal ............................................................................................................ 4-55 MODELL ÉS TÁROLÁS. ADATPERZISZTENCIA ................................................................................................. 4-63 Adatbázis séma szerinti modellek ................................................................................................ 4-64 Nézet jellegű modell..................................................................................................................... 4-66 AZ ÉRINTHETETLEN, GENERÁLT MODELL PROBLÉMÁJA ................................................................................... 4-67 EGYÉB MODELLATTRIBÚTUMOK ................................................................................................................ 4-68 EGY DEMÓ MODELL................................................................................................................................ 4-69 A KONTROLLER ÉS KÖRNYEZETE ................................................................................................... 5-71
5.1. 5.2. 5.3. 5.4. 5.5. 5.6. 5.7.
AZ ALKALMAZÁSUNK BEÁLLÍTÁSA. A WEB.CONFIG ........................................................................................ 5-71 AZ ALKALMAZÁS KIINDULÁSI PONTJA. A GLOBAL.ASAX ................................................................................... 5-73 ROUTING ............................................................................................................................................. 5-77 CONTROLLER ........................................................................................................................................ 5-88 ACTION ÉS PARAMÉTEREI ........................................................................................................................ 5-95 AZ ACTION KIMENETE, A VIEW ADATOK ...................................................................................................... 5-99 ACTIONRESULT ................................................................................................................................... 5-101
A VIEW MAPPÁK ................................................................................................................................. 6-121 A VIEW FÁJL KIVÁLASZTÁSA.................................................................................................................... 6-122 TARTALMA, TÍPUSOS VIEW .................................................................................................................... 6-125 PARTIAL VIEW..................................................................................................................................... 6-127 A VIEW-K EGYMÁSBA ÁGYAZÁSA ............................................................................................................. 6-133 A VIEW NYELVEZETE ............................................................................................................................. 6-139 Razor szintaxis ............................................................................................................................ 6-139 Kód a View-ban .......................................................................................................................... 6-143 Razor kulcsszavak ....................................................................................................................... 6-146 A VIEW KONTEXTUSA ........................................................................................................................... 6-150 BEÉPÍTETT HTML HELPEREK.................................................................................................................... 6-151 Nyers adatok. ............................................................................................................................. 6-152 Hivatkozás. ActionLink és RouteLink .......................................................................................... 6-153 Űrlap. BeginForm ....................................................................................................................... 6-154 Szövegbevitel. TextBox, TextArea .............................................................................................. 6-156 Label és formázott megjelenítés ................................................................................................ 6-159 Legördülő és normál lista ........................................................................................................... 6-160 Jelölők és rádióvezérlők CheckBox ............................................................................................. 6-163 Editor és Display template-ek .................................................................................................... 6-164 Partial és Render Partial ............................................................................................................. 6-176 Validációs üzenetek megjelenítése ............................................................................................ 6-178 URLHELPER ........................................................................................................................................ 6-179 ASZINKRON ÜZEM, AJAX ............................................................................................................ 7-183
7.1. 7.2. 7.3. 7.4. 7.5. 7.6. 7.7. 8.
KERETRENDSZEREK TÁRHÁZA .................................................................................................................. 7-183 A JSON ............................................................................................................................................. 7-184 JQUERY DIÓHÉJBAN .............................................................................................................................. 7-184 AJAX HELPEREK ................................................................................................................................... 7-187 AJAX HELPEREK DEMÓ .......................................................................................................................... 7-192 JSON ADATCSERE ................................................................................................................................ 7-201 AZ MVVM KERETRENDSZEREKRŐL.......................................................................................................... 7-212 A MODEL BINDER ....................................................................................................................... 8-215
8.1. 8.2. 8.3. 8.4. 9.
EGYSZERŰ TÍPUSOK ÉS A BEÉPÍTETT LEHETŐSÉGEK ....................................................................................... 8-216 FELSOROLÁSOK, LISTÁK ÉS SZÓTÁRAK ....................................................................................................... 8-223 BONYOLULT MODELLEK PROBLÉMÁI......................................................................................................... 8-230 MÉLYEN BELÜL .................................................................................................................................... 8-232 A BIZTONSÁG ÉS AZ ÉRTELMES ADATOK .................................................................................... 9-240
A RENDSZER BIZTONSÁGA ...................................................................................................................... 9-240 A FRONTVONAL ................................................................................................................................... 9-241 FELHASZNÁLÓ HITELESÍTÉS ..................................................................................................................... 9-245 Form alapú hitelesítés ................................................................................................................ 9-246 Windows alapú hitelesítés ......................................................................................................... 9-255 OAuth, OpenID ........................................................................................................................... 9-258 KÓDOLT AZONOSÍTÓK ........................................................................................................................... 9-261 VALIDÁLÁS ......................................................................................................................................... 9-273 A szerver oldalon ........................................................................................................................ 9-274 A kliens oldalon .......................................................................................................................... 9-285
REAKCIÓKÉPESSÉG, GYORSÍTÁS, MINIMALIZÁLÁS. ................................................................... 10-297 AZ OUTPUTCACHE ............................................................................................................................. 10-298 AZ ADAT CACHE ................................................................................................................................ 10-306 A BUNDLING..................................................................................................................................... 10-312 REAL WORLD ESETEK ................................................................................................................ 11-319 TÖBBNYELVŰ ALKALMAZÁS .................................................................................................................. 11-319 AZ ALKALMAZÁS MODULARIZÁLÁSA. AZ AREA. ........................................................................................ 11-327 MOBIL NÉZETEK, VIEW VARIÁNSOK ....................................................................................................... 11-333 SAJÁT HTML HELPEREK, MODELL METAADATOK ....................................................................................... 11-338 FÁJL LE- ÉS FELTÖLTÉS ......................................................................................................................... 11-343 DOLGOZZUNK EGYEDI VIEW SABLONOKKAL! ............................................................................................ 11-356 MVC 5 ÚJDONSÁGAI ÉS VÁLTOZÁSAI ....................................................................................... 12-358
1. Felvezető 1.1. Ajánló Azok számára írtam ezt a könyvet, akik még nem ismerik ezt a keretrendszert, vagy ismerik, de úgy érzik még nem eléggé (így magamnak is írtam :-). Lehet, hogy csak szeretnének egy átfogó képet kapni, vagy csak magyarul szeretnének olvasni egy egyébként angolul jól dokumentált rendszerről. Az elmúlt évek jó és rossz tapasztalata, a hozzám intézett kérdések és az ezek mögött rejlő ismeret hiányosságok indítottak arra, hogy egy jórészt gyakorlati szemléletű könyvet készítsek. Egy további oknak említeném, hogy a net tele van olyan ajánlásokkal, best-practice-ekkel, amik idejétmúltak és már nem érvényesek az MVC 4 verzióval kapcsolatban sem. Lesznek benne részek, amik túl alapszintűnek fognak tűnni, de a napi munkában jelentkező kérdések azt sugallták számomra, hogy még ezek sem tisztázottak rendesen, még néhány MVC környezetben fejlesztő számára sem. Minden bizonnyal lesznek nehezebb részek is, amik a későbbi fejlesztési munkák során valószínűleg előbukkanó problémákat járják körbe. Az MVC framework belső felépítése annyira rugalmas, hogy kevés olyan webes környezetben megjelenő igényt ismerek, amire ne lehetne legalább két jó megoldást is adni az MVC segítségével, többek között ezért szeretnék egy nagyon fontos dolgot tisztázni, amolyan garanciális feltételként: A könyvben szereplő megoldások, tippek nem biztos, hogy a legmegfelelőbbek minden helyzetre, tehát nyugodtan kételkedjen benne az olvasó. Nézzen utána, ha valami felbosszantja, ha butaságnak tartja. Járja be az utat, ami a tökéletes megoldáshoz vezet, és utána ossza meg, hadd okuljanak belőle mások is, és én is. Mert nincs az a szoftver és ismeret, amit ne lehetne feljavítani és bővíteni. Ilyen a természetük. A könyv tartalmi célja, hogy bemutassa az ASP.NET MVC-t, mint fejlesztési keretrendszert az alapoktól kezdve, a jelenleg kiadott 4-es verzión keresztül. Az MVC 5-ös verziója ebben a pillanatban preview állapotban van, tehát biztosat nem lehet róla mondani, de hogy legyen képünk arról, hogy mire számíthatunk, a fejezetek közé beékeltem az 5-ös verzió meglévő és várható újdonságait. Ezeket a felirattal láttam el. Ezek a szekciók még képlékenyek. Mivel a téma még bevezető szinten is nagyon szerteágazó, próbáltam a fókuszt az MVC-n tartani, ezért az MVC-hez egyébként jól illeszkedő technológiákról csak érintőlegesen lesz szó. A könyvben található példák - ritka kivétellel - csak memóriában tárolt adatokat fognak használni. A könyv címében levő ’+’, arra a tapasztalati és gyakorlati plusz kiegészítésre utal, amit a nyers alaptechnológiai ismeretek mellé tettem. Régebben a játékok örökélet és egyéb cheat módokat elérhetővé tevő "javító" programok neve után szerepelt a +, ++, +++ jel. Utalva arra, hogy így majd könnyebb lesz végigjátszani. Reményem szerint a könyv segítségével könnyebb lesz használatba venni az MVC keretrendszert.
1.2 Felvezető - A szerzőről
1.2. A szerzőről A 40-es éveinek az elején járó szoftverfejlesztő vagyok. A programozással 13 éves koromban ismerkedtem meg, mikor kaptam egy könyvet születésnapomra. Valamilyen Basic jellegű nyelvről volt benne szó, és biztos vagyok benne, hogy az ajándékozó nem tudta mit vett. A programnyelv nevére sem emlékszem már, csak arra, hogy elképesztő megszállottsággal vetettem bele magamat. Addig csak egy néhány lépésre képes, "programozható", szovjet számológépet próbálgathattam. Még TOS alapú számítógépet is csak a TV-ben láttam, így az egészet csak virtuálisan a fejemben tudtam elképzelni hardverestől-szoftverestől. Nagyon izgalmas volt, mert egy teljesen másik világban éreztem magam. A változók, regiszterek, goto-k, szubrutinok csak forogtak körülöttem. 14 évesen HT-1080Z1 csodagépen írtam az első kódokat az OMIKK-ban. Ekkor még sorba kellett állni a gépidőért… Az azóta eltelt időben elektronikával, 8-32 bites CPU-k hardverközeli programozásával, adatbázis- és szolgáltatáshátterű alkalmazásfejlesztéssel, webfejlesztéssel foglalkoztam. Írtam programokat mindenféle CPU-ra, mikrokontrollere Assembly-ben, C-ben. Ügyviteli, munkaügyi kisebb-nagyobb alkalmazást Visual Foxpro-ban, MFC+C -ben, Delphi-ben. Természetesen .Net környezetben is jó sokat. Jó darabig idegenkedtem a webfejlesztéstől. Majd egyszer mégis neki kellett állnom PHP alapú CMS rendszereket készíteni, integrálni, mert nem volt más az akkori cégemnél, aki meg tudta volna csinálni. Ekkor a HTML ismereteim még elég sekélyesek voltak, de egy évre rá HTML+CSS+Webszerver tematikájú kurzusokat tartottam tanfolyamokon. Annyira megszerettem ezt a világot, hogy hamarosan futószalagszerűen kezdtem gyártani a web site-okat, akkor még leginkább Drupal/PHP 2 alapokon. Majd mikor megjelent az ASP.NET MVC1.0 béta azonnal lecsaptam rá. Azóta egy bélyeggyűjtő hóbortosságával követem a változásait, fejlődését. Nem tudom megmagyarázni, de valamiért a benne levő architektúrát, a kódolási stílust, a képlékenységét, a fejlesztési szabadságot nagyon jónak érzem. Emlékeztet arra, amit 13 éves koromban tapasztaltam az orosz számológép után. Részben ez adott ihletet arra, hogy ezt a könyvet megírjam. Hasonló elszántságot és kitartást kívánva ajánlom tanulmányozásra a következő fejezeteket. Remélem, hasznát veszi a kedves olvasó.
Aláírás helyett, a digitális nyomaim: http://www.cornelius.hu http://blog.cornelius.hu https://www.facebook.com/kornel.regius http://hu.linkedin.com/in/regiuskornel
1 2
http://ht.homeserver.hu/ Ez egy nagyon innovatív, közösségi fejlesztésű, moduláris, open-source MVC platform
1-7
1.3 Felvezető - Hasznos dolgok
1.3. Hasznos dolgok A könyv legújabb verziója letölthető innen: http://bit.ly/mvc4plus Igyekszem majd a visszajelzések, javaslatok alapján kiegészíteni az aktuális verziót és időnként frissíteni a feltöltött tartalmat. Emiatt célszerű időnként letölteni az aktuális változatot.
A könyvben szereplő példakódokat szintén egyben le lehet tölteni. Ez egy egyszerű kétprojektes kódgyűjtemény. Nem alkot komplett összefüggő mintaalkalmazást, viszont számos helyen kiegészíti a könyv tartalmát, mert ami a könyvben csak kivonatos kód formájában szerepel, annak teljes terjedelmű változata itt megtalálható. Legtöbb esetben fapados „megvalósíthatósági tanulmányok” (POC) és csak arra jók, hogy break pointkokkal megállva tanulmányozhassuk a valódi működést, és hogy ne kelljen begépelni még egyszer. A példakódok között leginkább a modellosztályokra jellemző, hogy újrafelhasználásra kerültek és a téma kifejtése során átalakultak a kiinduló állapothoz képest. Emiatt előfordul, hogy a kódblokkok és attribútumok ki vannak kommentezve (de törölve nem). Ilyen esetben értelemszerűen vissza kell alakítani olyanra, amit az aktuális téma leír, igényel. A példakódok linkje: http://bit.ly/mvc4-app A letöltést a fájlt kiválasztva a helyi menün keresztül, vagy a fejlécmenü Letöltés linkéve kattintva lehet elindítani. Eltelhet 10-20 másodperc is mire a letöltés elindul. Lehetséges, hogy a projektek megnyitása után a Visual Studio még egy IIS Express-t is le fog tölteni, mert erre a fejlesztői webszerverre vannak beállítva. A példakódok megértéséhez szükség lesz a C# újabb lehetőségeinek alapos ismeretére is, mivel az MVC framework is erősen ezekre épít. Ha az olyan nyelvi sajátosságok ismerősen csengenek, mint ’partial class’, ’nullable típus’, ’opcionális metódus paraméter’, ’anonymous metódus', 'dinamikus típus’, ’lambda expression’, akkor azt hiszem nem lesz gond a példakódok megértésével. A könyvvel kapcsolatos javaslatok, észrevételek email címe: [email protected]. Idevárok minden véleményt. Jót is rosszat is, mert a semminél még egy negatív vélemény is jobb… Az MVC hivatalos oldala a http://www.asp.net/mvc, ahol sok hasznos, angol nyelvű oktatóanyag lelhető fel szöveges és oktató videó változatban. Reiter István jóvoltából egy további hasznos gyakorlati MVC bemutató (step-by-step) fordítását lehet elérni magyar nyelven ezen a linken: http://reiteristvan.wordpress.com/2012/03/06/asp-net-mvcegyszeru-webshop-lpsrol-lpsre-fordts-123-oldal/
1-8
1.4 Felvezető - A rövidítésekről, nevekről és jelekről
1.4. A rövidítésekről, nevekről és jelekről A könyvben a bevált hétköznapi terminológiát követem, ami lehet, hogy egyes helyeken magyartalannak tűnhet. Azokat a szavakat, amelyeknek nincs jó magyar megfelelője, angol kifejezéssel fogom leírni. Sokszor azt is, aminek van. Példának említem a request szót, aminek megvan a magyar megfelelője (kérés, lekérés), de azzal, hogy mégis az angol szót használom, érzékeltetni igyekszem, hogy adott helyzetben egy szigorúan technikai protokoll szintű akcióról és adatról van szó, és nem valami humán kérvényről. Fejlesztői körökben számos olyan szó van napi használatban, amik ezen stíluson is átlépnek. Csapatmunkában a ’rendereli’, ’lebildelem’, ’becsekkoltam’, szavak használata is azt mutatja, hogy ilyen körökben a gyors és hatékony munka érdekében a magyar nyelv bővítése gyakorlati okoknál fogva folyamatos. Igyekszem az ilyen szerkezeteket kerülni, de ha a kedves olvasó mégis megütközik ezen így nem tehetek mást, előre is elnézést kérek. Elkerülhetetlen, ezért lesznek betűszavak is, különösen olyanok, amelyek beváltak a hétköznapokban. Erre példa, hogy a Cascading Style Sheets megnevezét legritkább esetben láthatjuk egy szakkönyvben, helyette a fájlnév kiterjesztésének is használt ’CSS’ betűszó az elterjedt. Ugyan így van ez a 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.
3
http://tfs.visualstudio.com/ - Online ingyenes TFS. Egy verziókövető rendszer, csak ajánlani tudom.
1-9
2.1 Bevezetés - A tendenciák áttekintése
2. Bevezetés Az MVC első verziójának megjelenése óta jelentős változások mentek végbe a webes fejlesztési világban. Határozottan kiemelt fontosságú lett a kliens oldali interaktivitás, a felhasználói élmény fokozása, új platformok kiszolgálása és az MVC keretrendszer ebben egyáltalán nincs lemaradva. Ahhoz, hogy pozícionálni tudjuk az ASP.NET MVC technológiát érdemes lesz egy visszatekintéssel és általános megközelítéssel kezdeni. Sokat látott programozókban egy kimondott vagy kimondatlan kérdés szokott megfogalmazódni, ha egy eddig nem ismert betűszót lát. „Miért kell, megint egy új technológia?”. De könnyen el tudom képzelni, hogy a kérdést egy diploma előtt álló kolléga is ugyan ilyen természetességgel tudja feltenni. Mindenesetre egyáltalán nem új dologról van szó. Az ASP.NET MVC 1.0 évekkel ezelőtt elérhetővé vált (~2009) a .NET-es világ számára. Az MVC architektúra egy programozási környezet, független alkalmazástervezési minta, így más fejlesztési nyelvekben és környezetekben (Java, PHP, Ruby) is régóta hasznosítják az előnyeit az ottani keretrendszerek. Nagyon jellemző a használata a webes alkalmazások kialakításánál, ahol a fejlesztés eleve több programozási platformon folyik párhuzamosan. Ez a bevezető fejezet azokat a sarokpontokat gyűjti össze, ami az induláshoz szükséges lehet.
2.1. A tendenciák áttekintése Igencsak megváltoztak az igények az ASP / ASP.NET / PHP és más dinamikus HTML oldalgeneráló eszközök megjelenése óta. Az általánosan megnövekedett sávszélesség lehetővé teszi, hogy ne kelljen 1-10 kilóbájtokban számolni a képek és szkriptek méretét, mint mondjuk 10 évvel ezelőtt, amikor 1 megabájt letöltése hosszú percekbe telt. Ma nem ritka, hogy egy üzleti alkalmazás oldala 1-2 megabájt adatot küld át a böngészőnek csak azért, hogy az oldal kezdeti állapota megjelenjen. Még 2-3 kattintás és +1 megabájt töltődött le a gépünkre. Az unalmas HTML beviteli mezőket ötletes, mozgatható képi elemek váltják fel. Dinamikusan töltődő (autocomplete) legördülő listák, az oldal letöltése után a felhasználóval interakcióban kapják meg az elemeiket. Előtérbe kerültek az adatkeresést segítő felületi elemek, szűrők. A ma már magától értetődő dinamikus menütartalom mellett új, felbontás érzékeny elrendezési modellek jelentek meg. Nagyon fontossá vált az esztétika és az ergonómia, a felhasználói élmény pszichológiai vetülete. Néhány éve még az volt a kérdés a specifikáció összeállításkor, hogy a weboldalak 1024x768 vagy 800x600-ra méretre legyenek optimalizálva. Esetleg Internet Explorerre vagy Firefoxra? Ma egy ilyen kérdés esetén a megrendelő jogosan kérdőjelezheti meg a szakmai tudásunkat, hisz teljesen evidens, hogy jól kell működnie a 600x480-as felbontású mobileszközön, számunkra ismeretlen böngészővel is. De ha ez nem is lesz szempont, majd lesz az, hogy az Android alapú kütyüjére írt programot azonos adattartalommal tudjuk kiszolgálni, mint amit a 2 x Full-HD-s tabletjén a Safari böngészője megjelenít. Nem is olyan régen első hallásra meglepődtem, mikor egy nagyvállalat intranet alkalmazásánál a megrendelő megjelenési elképzelése mindössze annyi volt, hogy az új rendszerük legyen olyan, mint a Facebook. Nos, ez pedig nagyon lényegre törően fogalmazza meg azt, hogy hiába fejlesztik a Facebookot jóval többen (~3000) mint a mi fejlesztő csapatunk állománya, a mérce magasra van téve. Az adatoknak ma nem kis szigetekként kell elérhetőeknek lennie, hanem együtt kell működniük más rendszerek adataival, tartalmi hálózatot alkotva. Lehet, hogy az oldalunkon megjelenő táblázat egyik oszlopa egy teljesen más, tőlünk független szervertől származik, míg egy másik oszlop szintén más szervertől. És tudnék még fejtegetni és filozofálni, hogy mekkorát fordult a világ, de talán érzékelhető, hogy mások az igények ma, mint amikor az ASP.NET első kiadása megjelent.
1-10
2.2 Bevezetés - A webes alkalmazásokról általában
2.2. A webes alkalmazásokról általában Szükséges lehet néhány fogalom tisztázása, ismétlése, stb. Ha valami nem ismerős még ezek közül az alapfogalmak közül, akkor érdemes alaposabban utána nézni és csak utána továbbolvasni ezt, mert jó alapok nélkül nem sok hasznát lehet venni ennek a könyvnek. Szinte csak tőmondatokban és felsorolásszerűen néhány fontos pont az URL értelmezéséhez: Az URL alapesetben, a webkiszolgáló fájlrendszerében levő fájlt határoz meg, hasonlóan ehhez: http://peldaoldal.hu/index.html -> c:\inetpub\peldasite\index.html. Ez a szerver és a website konfigurációjának a függvénye. Ez az un. ’resource mapping’ mostanában ritkán ilyen egyszerű. Jellemzően az URL-t szakaszonként értelmezik és így lehetőség van a szakaszok szerinti mappa/erőforrás leképzésre. Erre egy példa: http://peldaoldal.hu/termekek/index.html -> c:\inetpub\kozostermekek\index.html http://peldaoldal.hu/csoportok/index.html -> c:\inetpub\focsoportok\lista.html Persze ez így kicsit erőltetett. Egy sokkal kézzelfoghatóbb alkalmazása, amit a ’friendly URL’ névvel szoktak illetni, és ami azt az eredeti ősi célt szolgálja, hogy az URL emberközeli és könnyen olvasható legyen. Unfriendly URL: http://peldaoldal.hu/termekek.aspx?csoport=butorok&alcsoport=szekek&labak=3 Friendly URL: http://peldaoldal.hu/termekek/butorok/szekek/labak/3 Talán nem szorul magyarázatra, hogy a második miért kellemesebb a szemnek és a SEO 4 szempontoknak sem árt, ha így néz ki. A friendly URL-hez még hozzá tartozik, hogy ebből nagyon könnyű a webkiszolgáló számára érthető paraméteres (query stringes) URL-t képezni. Ennek a módszernek a neve: URL rewrite. Ahhoz, hogy a speciális böngészőkéréseknek speciálisan tudjon válaszolni a szerver, bővítményekkel (plugin, handler, module) lehet kiegészíteni. Ezek a bővítmények képesek a webszerver normál kiszolgálási mechanizmusát a speciális kéréstípusnak megfelelően lekezelni. Ezek a kéréstípusok leginkább a kért erőforrás fájlnév kiterjesztése alapján kategorizálhatóak. Így például lehet olyan (HTTP) handlert készíteni, ami dinamikusan generálandó képfájlt tud készíteni. Például olyan képet, ami tartalmazza a mai dátumot, ahelyett hogy a webszerver a fájlrendszerben tárolt fájlt szolgálna ki. Szintén megoldhatjuk egy ilyen handlerrel, hogy a böngészőnek küldendő HTML oldalt memóriában állítsuk össze, oldalsablonok alapján. Hogyha ezt tovább gondoljuk és hozzáadjuk az URL rewrite képességet, amivel még a fájlnév hivatkozást sem kell elvárni az URL-ben (nem kell index.aspx-re, index.php-ra referálni), akkor a webszerver alapműködését alaposan át tudjuk formálni. Azt is megtehetjük, hogy a nyers fájlkiszolgálás lesz a ritkább eset és leginkább dinamikusan generáljuk az oldalakat sablonok alapján. Ezt csinálja lényegében az MVC is. A HTML oldalakat dinamikusan állítja össze és az oldal által igényelt
4
Search Engine Optimization – Az oldalunk optimalizálása, hogy a tartalom jól értelmezhető legyen a kereső motorok számára, Google, Yahoo, stb.
1-11
2.3 Bevezetés - Böngésző – szerver interakció további képeket, CSS fájlokat pedig statikus fájlként szolgálja ki, hagyományos fájlnév-erőforrás leképzéssel. Érdemes azt is látni, hogy egy oldal letöltése nem áll meg a megcímzett HTML tartalom letöltése után - ami jellemzően nem csak 50-200Kbyte nagyságrendű adatot jelent – hanem folytatódik a letöltés tovább. Letöltődnek a CSS, JS fájlok és a képek. A szkriptekben levő kódok aktiválódnak és további képeket, HTML szakaszokat töltenek le. Mire az 1db oldalunk tartalma megjelenik, már lezajlott nagyságrendileg 5-100 további HTTP kérés (request), amelyek összességében megabájtban kifejezhető adatmennyiséget jelentenek. Erre a szerverünknek van kb. 3 másodperce. Ez utóbbi adat egy ajánlás, mert a 3. másodperc várakozás után a látogatóink szubjektív megítélése rohamosan billen át a negatív tartományba. A 10. másodpercnél pedig az oldalunkat tudattalanul is leírják. Ezek miatt fontos olyan rendszerekben gondolkodni, ami ezt az igénycsoportot jól ki tudja elégíteni:
Az oldalnak sok olyan képi elemet kell tartalmaznia, ami miatt vonzó lesz a tekintet számára A lehető leginteraktívabb legyen, ami sok és összetett böngészőben futó javascriptet jelent. Minden álljon készen 2-3 másodperc alatt. Legyen biztonságos Legyen könnyen programozható, bővíthető.
Véleményem szerint az ASP.NET MVC jó esélyt ad, hogy hatékonyan meg tudjunk felelni ezeknek a kihívásoknak.
2.3. Böngésző – szerver interakció A kliens (böngésző) néhány ’ igén ’, METHOD-on keresztül intézi a kéréseit. Ezek a parancsok lefedik az általános adatkezelés (CRUD) igényeit. GET, PUT, POST, DELETE, HEAD, stb. A két legfontosabb: -
A GET, amivel egy oldalt tudunk elkérni a szervertől az URL-en keresztül. A GET-nek csak két paramétere van: az URL a paramétereivel és a verzió szám. GET http://domainev.hu/termek/10?q=vendeg HTTP/1.1
-
A másik metódus a POST, amivel leggyakrabban a böngészőben levő kitöltött adatlap (form) mezőinek az értékét tudjuk visszaküldeni feldolgozás céljából, a szervernek. Nyilvánvalóan ennek is van URL-je, de az nem szokott tartalmazni paramétereket (de nem kizárt, ahogy az sem hogy egyes böngészők ezt nem támogatják). A form input mezői (a neve és tartalma) a HTTP csomagban vannak. A post csomagot nem csak HTML formmal lehet előállítani, hanem JS kóddal is.
Ha a fenti HTTP igéket és az erőforrásokat reprezentáló URL-eket szervezetten használjuk, tehát ha a HTTP method lesz az ige és az erőforrásunk a tárgy, akkor a rendszerünk nem lesz REST úgy használni a web-et, ahogy azt eltervezték. Nagyjából ez a REST jelentése is. Példaként adott egy URL: http://domainnev/termek/1, ami az 1-es azonosítószámú terméket teszi elérhetővé. A HTTP methoddal pedig közölhetjük, hogy mit tegyen a szerver ezzel az 1-es számú termékkel. Letöltse (GET), frissítse az adatait (POST), törölje (DELETE). Postback-nek szokták nevezni azt a szituációt,
1-12
2.4 Bevezetés - Az előzmény. Az ASP.NET Web Forms amikor GET-el lekért oldalba ágyazott form adatait visszaküldjük egy POST requesttel. Ez a terminológia a Web Forms fejlesztéssel kapcsolatban igen képszerű. Mivel a teljes oldal egy komplett form, ami azonos URL-re lesz visszaküldve, mint ami az oldalt előállította. A Web Forms esetén a GET és a POST request feldolgozása a Page Load eseményen megy keresztül és ott általában egy elágazást kell készíteni a feldolgozásban, hogy éppen GET vagy POST(back) szituációban vagyunk. Az MVC esetében ezt a szituációt szűrők választják ketté így számunkra a tisztán GET és POST esetén induló metódusok fogják feldolgozni a requestet. Mivel igen gyakori, hogy más URL-re, és szinte biztosan másik feldolgozó metódushoz érkezik meg a POST csomag, mint ahonnan szármázik, ezért a postback szó nem teljesen megfelelő MVC esetében, ezért nem is fogom használni. A HTTP protokoll jelenlegi formájában állapotmentes (stateless), ami azt jelenti, hogy a klienstől induló kérésre a szerver válaszol és elküldi az URL-ben kért erőforrást (fájlt), és utána így protokoll szinten nem emlékeznek egymásra. Nincs sorszám, emlékeztető, Id, stb. A szerver - segédeszközök nélkül - nem tud összefüggést találni az azonos böngészőből, azonos felhasználótól jövő két egymás utáni kérés között. Szerencsére van egy sessionazonosító cookie, amit az ASP.NET az első válaszához hozzáfűz, ha az egymás utáni oldallekérések (a requestek) között a felhasználóhoz kötött, megmaradó adatokat szeretnénk tárolni a szerver oldalon. Ha a kliens a következő kérésébe ezt az azonosítót szintén belefűzi, akkor a szervernek meg lesz a referenciája az előző kérésre, hisz abba, ő fűzte bele a saját maga által kiokoskodott számot. Ez a sessionazonosító egy Session példányt azonosít. Ebben tetszőleges adatot tárolhatunk a szerveren (memóriában, fájlban, adatbázisban).
2.4. Az előzmény. Az ASP.NET Web Forms Bizonyára 1000 érvet fog tudni felhozni egy tapasztalt ASP.NET programozó, hogy az ASP.NET Web Forms „mindenre elégséges” (amúgy ez egy isteni jelző), hisz évek óta tapasztalja, hogy meg lehet oldani azzal a platformmal is mindent (ha meg nem, majd alkalmazkodik az ügyfél ). Nézzük meg, hogy mik azok a problémás részek egy ASP.NET + Web Forms rendszernél!
A kezdeti alapötlet arra épült, hogy a Windows Forms/MFC/Delphi környezetben felnőtt szakemberek, hogyan tudnák a dizájnerrel támogatott RAD (gyors alkalmazásfejlesztés) metodikában, rutinosan használt fogásaikat megtartva, gyorsan átállni a webes fejlesztésre. Nem mondhatjuk, hogy ez nem volt sikeres. Az a lehetőség, hogy a szerkesztői felületre dobhatjuk a vezérlőt és néhány kattintás után egy-két propertyt beállítva kész az oldal, nagyon rapiddá teszi a fejlesztést. Ott van még az eseménykezelés, az egyszerű adatkötés, a felületre húzható adatszolgáltatók. Ilyenekről egy PHP-s fejlesztő nem is álmodik. Remélem, nem ijeszt el senkit, de már most elárulom, hogy az ASP.NET MVC sem támogatja ezeket. Ugyanis ez egy másik szempontot támogat, a jól kézben tartott kliens oldali kódot, és a funkcionális rétegek elszeparálását. A fejlesztő pedig tanuljon meg HTML-t és JS kódot írni… Amikor megnézünk egy kész, főleg régebben készült ASP.NET Web Forms alkalmazást, két feltűnő dolgot lehet észrevenni a generált HTML kódban. Az egyik, hogy a kód kicsit kusza és tele van beékelt javascript kódblokkal. Emiatt nem túl egyszerű hibát keresni benne. A másik, hogy szinte minden felületi elrendezést
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.
1-13
2.4 Bevezetés - Az előzmény. Az ASP.NET Web Forms
5
Az előzőhöz kapcsolódva van egy harmadik szembetűnő dolog is, amit úgy hívnak, hogy viewstate. Erről is lehet jót és rosszat is mondani, a lényege az, hogy az oldal vezérlőinek az állapotát tartalmazza. Ma már az ASP.NET Web Forms is úgy szereti, hogyha ezt csak korlátok között használjuk, ugyanis ebbe bele kerülhet pl. az oldalon található táblázatvezérlő összes lényegi adata. Ez, és más hasonló okokból kifolyólag ez az adathalmaz, igen méretesre tud nőni. A jó hír, hogy az MVC-ben nincs viewstate, de ez egyben a rossz hír is, ugyanis manapság minden böngésző támogatja a többfüles böngészést, és ezt a felhasználók ki is használják. Emiatt sok esetben előfordul, hogy valamilyen formában szimulálni kell MVC-ben egy viewstate jellegű viselkedést, hogy a több böngészőablakba lekért oldalak megkülönböztethetőek legyenek az állapotuk szerint. Kevés az az MVC oldal, amin nincs legalább egy hidden HTML mező… Az ASP.NET Web Forms egy HTML formot támogat. Innen kell elindulni és ezzel kell együtt élni. Az MVC-nél nincs ilyen megkötés, mivel a HTML szabványban sincs. Egy oldalra sok űrlapot is kitehetünk, a kérdés, hogy a mai AJAX trendek mellett, erre szükség van-e egyáltalán. A tradicionálisan alkalmazott User Control-ok, Webpartok logikája arra a gyakorlatra épült, hogy az oldal egésze egymenetben generálódik. Igaz léteznek AJAX kontrolok, de ezek mennyisége, képessége elmarad más javascript keretrendszerek (Mootools, jQuery, Dojo) képességétől, választékától5, amiket sok más fejlesztői környezetben vagy CMS rendszerekben beváltan használnak. Ha csak arra gondolunk, hogy az ilyen ’ más ’ keretrendszereknek milyen méretű felhasználói/programozói/tesztelői tábora van és mekkora how-to példamennyiséggel rendelkeznek és ezzel mennyi próbálkozástól (négyszemközt erősebb kifejezést használnék) kímélhetjük meg magunkat, már ez is nyomós indok lehet egy technológiai váltás mellett. „De hát lehet jQuery-t használni ASP.NET alatt is!” Valóban, de ezek nem voltak gyerekkori játszótársak, csak az ASP.NET 4.0 óta barátkoznak. Pedig ez fontossá válik akkor, ha tényleg elkezdjük együtt használni őket, főleg, ha négyet: ASP.NET + AJAX + jQuery + jQuery-UI használunk egyszerre. Mondjuk azért, mert megtetszik a jQuery-UI egységes ablakkezelési módja és egységes kinézete, ami valahogy nem illeszkedik az ASP.NET theme/skin rendszeréhez. Aztán jönnek még további érdekességek, mikor szembe találhatjuk magunkat azzal is, hogy az ígéretesnek tűnő jQuery a szelektorjában valami egyszerűt vár pl.: ilyet: $(’#textbox1’), viszont az ASP.NET HTML Id generátora ennél sokkal ravaszabb ID-ket generál automatikusan , például.: ilyet: ’ct100$KozepPlaceHolder$SajatVezerlo2$Textbox2’. Aztán továbbmegyünk és elgondolkozunk, hogy valóban jó megközelítés, hogy a document betöltése után induló eseményre, a DocumentReady-re egy oldalon 4-5x is feliratkozunk a különböző user controlok miatt? És így tovább. 5-10 éve egy HTML oldal betöltés végeredménye kb. olyan volt, hogy a letöltött fájltípusok aránypárja így nézett ki: HTML : (JS+CSS) = 5:1, ma meg kb. így: HTML : (JS+CSS) = 1:10. Ez pedig azt jelenti, hogy a kliens oldalon igen tetemes kód fut és a grafikus dizájnerek sem tétlenkednek mostanában. Hogy ebből ne legyen káosz, ugyanazt az elvet kell alkalmazni, ami a c# kódolásnál már evidens: tervezési minták, strukturált/rendezett átlátható kód, egységes elnevezési konvenciók. Szinte kényszerű, hogy a HTML kód elkülönüljön a CSS stílusoktól és a JS kódtól, amennyire csak lehet. Az, hogy a un. ’Separation of Concepts’ elv szerint „az alkalmazásban levő funkcionális egységek a lehető legkisebb mértékben függjenek egymástól” kicsit nehezen teljesíthető egy hagyományos ASP.NET-es megközelítésben, ahol az például az SQL adatforrást az .aspx oldalba
2.5 Bevezetés - ASP.NET MVC platform előnyei (a html sorok közé) szokták beékelni (sok-sok oktatóanyag példája alapján…). A Dependency Injection használata és a Unit tesztelés MVC-ben lényegesen egyszerűbb. Ide kívánkozik, hogy az ASP.NET Web Forms 4.0 verzió megjelenésével nagyon sokat fejlődött. Belekerültek olyan szolgáltatások, amik az MVC-ben debütáltak előzőleg. Szóval már nincs olyan nagy differencia, mint mondjuk a 4.0 előtti Web Forms és az MVC 3 között. A helyzet bizonyára tovább javul a 4.5 után megjelenő verziókkal, és a különbségek is csökkennek majd. A következő generációs Web Forms oldalsablonok is már sokkal szofisztikáltabban különítik el a HTML markupot a háttérkódtól.
2.5. ASP.NET MVC platform előnyei Nézzük meg miben más vagy jobb az MVC keretrendszer. -
-
-
-
-
-
-
Az MVC forráskódja a kezdetektől hozzáférhető a CodePlex-en. Nem függünk attól, hogy mikor javítanak ki egy hibát a frameworkben, ha megtaláltuk, akár magunk is kijavíthatjuk. Sőt, akár a fejlesztésnek is részesei lehetünk, több módon is. Ez a lehetőség jól fog jönni majd akkor, ha saját kiegészítőket kezdünk írni. Az MVC-vel kapcsolatban soha nem volt szükség arra, hogy majd egy ASP.NET Guru megmondja mit és hogyan kell megoldani. Ott a forráskód. Meg lehet nézni, le lehet fordítani. Akár végig lehet lépkedni a debugger-el a teljes oldalfeldolgozáson. (ASP.NET 4.0 óta a Web Forms is open-source ) Az MVC az egy évtizede folyamatosan fejlődő, javuló ASP.NET kódjára épül. Tehát az alapok igen jól teszteltek, hatékonyak és hibatűrők. A request, a response, a session objektumok, a cache kezelés és a security szempontjából teljesen a tradicionális ASP.NET alapokra építkezik. Az MVC-ben bevált technikák annyira jól sikerültek, hogy visszahatottak az ASP.NET alaprendszerre is és annak is integráns részei lettek. Ilyen például a routing és a model binder megvalósítása. Könnyen integrálható bármely JS keretrendszerrel és az MVVM mintával. Minden további nélkül egy alkalmazásban lehet használni ASP.NET Web Forms és MVC oldalakat. Tehát a meglévő projektek technológiai váltása nem vagy-vagy alapon zajlik, hanem lehet apró lépésekben is átmigrálni. Mivel a kód és a megjelenítés rendkívül jól el van szeparálva, kevés szoros függőség van a modulok között, ezért nagyon egyszerű automatikusan tesztelni. Nem kerül a generált HTML kódba semmi olyan, amit nem mi raktunk oda. Precízen kézben tudjuk tartani az egész folyamatot, az elemek elnevezését, és a javascriptek eseménykezelését is. Az egész MVC framework úgy épül fel, hogy adja magát, hogy azokat az irányelveket alkalmazzuk, amelyek bármely többrétegű architektúrában melegen ajánlottak. Teljesen természetes, hogy ORM mappert fogunk használni, szolgáltatásokat fogunk hívni az alkalmazásunkból. Az oldalainkat hierarchikus template-kből állítjuk össze és a modelljeink fogják tartalmazni a validációs szabályokat. De ezekre nincs megkötve a kezünk. Ha nem tetszik valamelyik része a frameworknek megvan a módja, hogy lecseréljük azokat kedvünk szerint. Ha nem jó nekünk a View sablonunk nyelve, hát írhatunk egy másikat. Semmi sem köt minket ahhoz, hogy az ASP.NET-ben megszokott <%%> közé írjunk vagy a Razor szintaxis szerint a @ után írjunk kódokat. Valójában az MVC framework képességeit a legalacsonyabb szinttől kezdve felülbírálhatjuk.
1-15
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ű.
2.6. Az ASP.NET és MVC framework Ha ránézünk erre a technológiai blokksémára, látható, hogy az MVC az ASP.NET alaprendszerre épül rá, hasonlóan a zöld sávban levő többi technológiához.
Régebben az ASP.NET és a Web Forms gyakorlatilag egyet jelentett. A .NET 4.0 megjelenésével együtt átalakult a technológiai platform. Az MVC 4.0 és a Web Forms 4.0 két fejlesztési alternatíva lett. Az MVC mellett megjelentek további lehetőségek, amikkel ez a könyv nem foglalkozik. Ezek a fenti ábrán is megtalálható Single Page Apps, WebAPI és SignalR platformok. Viszont pont e könyv írásának idején jelent meg Reiter István jóvoltából a WebAPI6-ról egy könyv, amit mindenképpen ajánlok az olvasó figyelmébe, miután túljutott e könyv olvasásán.
2.1. Az MVC komponensei és beszerzésük. A kalandtúra első lépése, hogy összeszedjük a felszerelést. Elsőként szükség lesz egy megalapozott C# tudásra. A példakódok könyvben kizárólag ezen a nyelven fognak szerepelni. Szerencsére rendelkezésre áll magyar nyelven több kiváló szakkönyv is. A példák Visual Studio 2012-ben lesznek bemutatva, ezért szükségünk lesz egy Visual Studio példányra. Ha nem áll rendelkezésre, talán megéri a könyv elolvasásának az idejére egy próbaváltozatot7 letölteni és használatba venni. További lehetőség a Visual Web Developer Express 8 , ami ingyenesen használható. Az MVC 4 framework a VS 2012-vel együtt települ fel a gépre így ezzel nincs semmi dolog. A Visual Studio ezt megelőző 2010-es változatához illeszkedő telepítőjét a http://www.asp.net/mvc/mvc4 oldalról érdemes letölteni. Itt található kétfajta telepítő. Az egyik a Web Platform Installer alatt
2.2 Bevezetés - A böngészőkről működik és jelentősen leegyszerűsíti a környezet beállítását és a további szükséges kiegészítők letöltését is. Ezt ajánlott használni. A másik a standalone telepítő, amivel szintén lehet telepíteni, de a függőségekre ekkor nekünk kell figyelni, és egyesével kell telepíteni azokat. Visual Studio 2010-es esetén szükséges, hogy a Visual Studio 2010 Service Pack 1 előzőleg már telepítve legyen. A működéshez a .NET 4 és a PowerShell 2.0 (minimum) szintén alapfeltétel. Akik pedig mélyebben szeretnék tanulmányozni az MVC működést azoknak érdemes letölteni az MVC 4 és 5 forráskódját innen: http://aspnetwebstack.codeplex.com/ . Mint arról már szó volt az MVC túl jól sikerült elsőre is ahhoz, hogy ne csak egy ASP.NET kiegészítőként élje az életét. Ezért a CodePlex projekt neve sem az, hogy MVC, hanem az ASP.NET-re ráépülve: Asp.net Webstack. A régebbi (v1, v2, v3) verziók még elérhetőek a http://aspnet.codeplex.com/releases oldalról. Ha nem sajnáljuk rá az időt, letölthetjük a régebbi verziók forráskódját és összevethetjük az MVC 4-el és rögtön szembe fog tűnni, hogy ez a legújabb változat már tényleg nem csak egy árva projekt mint régebben, hanem az ASP.NET rendszer szerves része lett.
2.2. A böngészőkről Mivel web fejlesztésről lesz szó, kelleni fog legalább két olyan böngésző, amit jól ismerünk. Tehát nem csak annyira, hogy tudjuk, hova kell beírni az URL-t, hanem alaposan. Olyanra gondolok, amelyben tudjuk, hogy
Hol kell beállítani a preferált nyelvet Hol kell kitörölni a böngészési előzményeket és a cookie-kat. Hol kell letiltani a javascript futtató motort Hogy lehet elővarázsolni a belső diagnosztikai lehetőségeket.
Kezdő MVC fejlesztőnek ajánlott az Internet Explorer (IE) legújabb verziója, legfőképpen azért, mert fejlesztés során remekül együttműködik a Visual Studio-val, míg ez a többiről nem mondható el. A következő részekben viszont a Google Chrome-ot, a FireFox-ot is fogom használni a példák során. Ezek, beleértve az IE-t is, közös jellemzője, hogy mindnek van belső diagnosztikai modulja. (A FF-hoz a FireBug9 nevű bővítményt érdemes letölteni.) Ha még nem ismerjük ezeket a weboldalak tartalmát feltáró eszközöket, akkor egyszerűen nyomjuk meg az F12-őt a kedvenc böngészőnkben és nézzük meg mit kapunk. A FF FireBug bővítménye:
A tabokon kategorizálva láthatjuk az aktuális oldal tartalmát, ami nagyságrendekkel jobb mintha a nyers oldal forrását nézegetnénk. Ha nincs még alapos ismeretünk arról, hogy a böngészés közben mi
9
http://getfirebug.com/downloads
1-17
2.2 Bevezetés - A böngészőkről is történik a háttérben, ezzel az eszközzel nagyon sok tapasztalatot tudunk szerezni. Nyissuk meg a ’Net’ fület és töltsük újra az oldalt! Általában ne a és ehhez hasonló ikont használjuk oldalfrissítésre. Ezeket nem a fejlesztőknek szánták. Van helyette F5 billentyű és Ctrl+F5 kombináció is. Böngészőnként kicsit eltérnek, de hatásukra újratöltődik az oldal, de a Ctrl+F5 FireFox esetében kényszerítetten újratölti a helyben tárolt adatokat is. Ezt érdemes kipróbálni a Net fülön (tab-on). Egy gyakori kezdő fejlesztői hiba, hogy az átírt CSS vagy JS kód nem azt csinálja amit módosítottunk, hanem makacsul az előző változatot hozza. Ennek oka, a böngészőben levő gyorsítótárazás. Ennek eredménye bekeretezve látható a következő ábrán "304 – Nincs módosítva". Ami azt jelenti, hogy a tartalom a helyi gyorsító tárból jött és nem a szerverről, ahova feltöltöttük a legújabb verziónkat. Erre érdemes figyelni a tanuláskor, fejlesztéskor és a webszerver beállításakor!
Ezek a böngészőbe épített inspektorok annyira hasznosak, hogy komolyan ajánlom, hogy bármilyen webes fejlesztés elkezdése előtt, előtanulmányként alaposan ismerjük meg egyet. Van még egy eszköz, amit érdemes használni: ez a Fiddler10. Bár a legtöbb esetben az előbb említett diagnosztikai modulokban elérhető hálózati eseményeket naplózó lista elégséges, ajánlott mégis a Fiddler beszerzése (ingyenes). Ezzel a HTTP forgalmat egész részletesen meg tudjuk figyelni, sőt vissza is tudjuk játszani, így akár böngésző nélkül is tudjuk tesztelni az alkalmazásunkat. Segítségünkre lehet biztonsági rések felfedezésében. Például, ha következmények nélkül vissza tudjuk játszani az előző HTTP post eseménysort, akkor az esetleg biztonsági hiba is lehet. Egy fontos tanács webfejlesztőknek: "Ne bízz a böngészőben!". Nem olyan rég majdnem fellélegezhettünk. Úgy tűnt, hogy azzal, hogy az Internet Explorer 6-os verzióját kivonják a forgalomból, megszűnnek a böngészők közti kompatibilitási eltérések. Végre minden böngésző betartja a szabványt, de nem. Erre soha se alapozzunk. Ahogy eddig is voltak eltérések, úgy most is vannak és nyilvánvalóan ez után is lesznek. A HTML 5 és CSS3 összes képessége még nincs egységesen, kiforrottan implementálva a böngészőkben. A javascript motorok is igen képlékenyen változnak ilyen-olyan irányban. Érdemes elgondolkodni azon, hogy a Html 4.01-es verzióját is csak átmeneti szabvány (transitional mód) szerint használják a mai napig is a legtöbb helyen. Sok-sok elavult HTML taggel. Az olyan közeget, ahol a múlt sincs rendesen lezárva és velünk él, a jövő/jelen is bizonytalan, nem hívhatjuk stabilnak. Ha úgy tekintünk a webes szabványokra, mint ajánlásokra és nem kőbevésett szabályokra, akkor sok frusztrációtól kímélhetjük meg magunkat az olyan esetekben, amikor valami nem úgy jelenik meg a megjelenítőn, ahogy kéne. Innentől viszont kezdjünk el foglalkozni a közelmúlt egyik legnagyszerűbb webfejlesztési technológiájával, amit a Microsoft kiadott.
10
http://www.fiddler2.com
1-18
3.1 Első megközelítés - Az MVC architektúra
3. Első megközelítés Ez a fejezet amolyan bemelegítés, ismerkedés, bemutató célzatúnak készült alapozó. A fejezet végén már el lehet kezdeni kipróbálni az MVC lehetőségeit, sőt javasolt is egy pici alkalmazást felépíteni önállóan. A rákövetkező fejezetek alaposan nagyító alá veszik a fő komponenseket, ezek együttműködését, a lehetőségeket, elmerülve a technológiai részletekben.
3.1. Az MVC architektúra Az MVC (azon túl, hogy nyilvánvalóan egy betűszó, ami a Model-View-Controller hármas kezdőbetűiből áll) lényegében egy olyan tervezési minta, aminek alapja az, hogy a program alapvető szerepei jól elkülöníthető egységekbe vannak szervezve. Van egy modellünk (M), ami lényegében az adatunk leírója, van egy nézetünk (V), ami meghatározza, hogy a modellünk hogyan jelenjen meg és van egy kontrollerünk (C), ami kezeli az előbbi kettőt és reagál a böngészőből érkező kérésekre. Tehát, ha ezt a technológiát szeretnénk használni a tervezés/programozás során, akkor érdemes a gondolkodásunkat is ehhez alakítani. Kis paradigmaváltást igényel egy Web Form fejlesztés után, ami általában nyűgös, de azt mondhatom, hogy megéri az átállást. A programozás rövid történelme alatt áttekinthetetlen, nehezen bővíthető és karbantarthatatlan forráskódok sokasága született. Fejlesztések húzódnak a végtelenségig, rendkívül rossz hatékonyság és magas költségek mellett. Alkalmazások sokasága készült el úgy, hogy az üzleti logika a megjelenítéssel foglalkozó osztályába van beledrótozva, mondjuk az ASP.NET oldal háttér-kód fájljába, sok esetben úgy, hogy a közvetlen adatbázis elérés is innen történik. Mindezek azt eredményezték, hogy karbantarthatatlan kódmaszlagból épültek kritikus alkalmazások. Számos módszertan született ennek kivédésére, de ezek közül az egyik legfontosabb, hogy az alkalmazást funkcionális részekre érdemes bontani és többrétegű felépítést célszerű választani. Ebben ad nagyszerű alapot az MVC tervezési mintára épülő technológia11. Minden lehetőség adott, hogy az alkalmazás felelőségi köreit jól elszeparáltan tudjuk implementálni és szakítsunk a régi, mindent egy helyre zsúfoló megoldásainkkal. A tervezésben és később a megvalósításban pedig nagy segítség, ha a tervünket jól elkülöníthető egységekre tudjuk bontani, amely egységek külön-külön tovább tervezhetőek és/vagy magukban kivitelezhetőek, lecserélhetők, sőt kipróbálhatóak. Manapság egy web alkalmazás elkészítése igen sok tervezési, fejlesztési és technológiai területet érint. A teljeség igénye nélkül: szolgáltatás orientált és kommunikációs architektúrák, adatbázisok a maguk szerteágazó ismeretigényével, böngésző és mobil specialitások és egy sor további nyelv az alap forrásnyelv mellett (HTML, JS, CSS, T412). A M-V-C hármas tagolás nagyban segíti a szakterületekre specializálódott fejlesztői munkacsoportok kialakítását. Így lehetőségünk van arra, hogy a View megvalósításánál olyan szakértőket alkalmazzunk, akik a felhasználó élmény, a szép és interaktív felületek kivitelezésében jártasak, ők a front-end fejlesztők. Míg egy másik csapat, a back-end fejlesztők, a front-end-et kiszolgáló szolgáltatások és adatsémák megalkotásában zsonglőrködnek. Ügyes tervezéssel pedig biztosítható, hogy a csapatok munkája közel párhuzamosan haladjon. Hamarosan nyilvánvalóvá fog válni, hogy egy View megtervezése és megalkotása számos további technológiát igényel, amik együttesen elég nagy
11
Ez nem csak az ASP.NET MVC-re igaz, hanem a minta következménye. Idézet a Java világból: "A Velocity kikényszeríti a Model-View-Controller (MVC) fejlesztési stílust, elszeparálva a Java kódot a HTML sablontól. Nem úgy, mint a JSPs". 12 T*4 -> Text Template Transformation Toolkit. Fájlkiterjesztése: .tt
1-19
3.1 Első megközelítés - Az MVC architektúra
1-20
tudásigényűek és eltérő jellegűek ahhoz, hogy egy üzleti intelligencia vagy WCF szolgáltatások programozásában jártas fejlesztőt, nem biztos hogy érdekelni fog (vagy nem lesz hatékony) és viszont. Nos, ezek után nézzük meg az oly sok helyen fellelhető és valószínűleg ismerős hármas tagozódás sémáját.
Controller
Model
View
Ez az ábra nem tartalmaz köröket és nyilakat mint máshol szokott lenni, ugyanis szerintem ez nem lényeges, sőt egyes rajzok megtévesztőek is. A fontos hogy lássuk, hogy van három jól elkülöníthető funkcionalitású blokk (mackó sajt), ami külön-külön részegységeit alkotja a fő feladatnak, hogy kiszolgáljunk egy dinamikus weboldalt. Az MVC-nél a működési folyamatot, más néven az oldal életciklusát, a böngészőből érkező request indítja el. Az MVC framework e kérést a kontrollerhez juttatja, annak is egy metódusához, amit actionnek13 szoktunk nevezni. A metódus létrehozza a modell példányt, majd meghatározza, hogy a modell alapján az MVC melyik View-t használja fel a böngészőnek küldendő válasz (a response) létrehozásához. A következő ábra szemlélteti, hogy az általunk implementálható M-V-C hármasunk valójában az MVC framework által szorosan felügyelt folyamat részállomásaiban kerülnek felhasználásra.
Controller példányosítás. Action kiválasztás.
Controller+Action
Modell a View számára
Response visszaküldése
View +Modell
View feldolgozása
A mi M-V-C alkalmazásunk
Modell a request alapján
View + Layout kiválasztás.
MVC framework
Request http://localhost/ Home
Response A kész html oldal
Az ábra csak egy elemi, alapszintű műveletsort szemléltet, a valóság ennél kicsit összetettebb, de erről is lesz majd szó. Az ASP.NET MVC framework az MVC tervezési minta egy olyan részleges megvalósítása, amiben az infrastruktúra lényeges részeit már leprogramozták helyettünk. Nem kell 13
Sajnálatos módon az „action” szó igencsak „túlterhelt”. Ez a neve a HTML formot feldolgozó URL attribútumnak és a .Net generikus, paraméter nélküli delegate-jének. Ez utóbbi azonban szóba sem kerül később.
3.2 Első megközelítés - A Modell
1-21
bíbelődnünk azzal, hogy a request típusmentes adatait (URL, input mezők, stb.) egy osztálypéldányban kapjuk meg vagy esetleg metódusparaméterekként. Nekünk csak meg kell írni a kontrollert a metódusaival, de azok hívásáról az MVC rugalmasan paraméterezhető, aktiválórendszere gondoskodik. Láthatjuk majd később, hogyha valamelyik előre megvalósított szolgáltatása nem megfelelő, akkor elég egyszerűen lecserélhetjük. Ez annyira igaz ezzel a keretrendszerrel kapcsolatban, hogy néha az az érzésem támad, mintha ezt is akarnák a fejlesztői. Mikor jelenlegi webes alkalmazásokat egy skálán próbáljuk elképzelni, akkor a skála kezdetére tehetjük azokat a megvalósításokat, amik egy menetben generálnak egy nagy HTML oldalt és ezt egy nagy HTTP post alkalmával dolgozzák fel (jellemzően a lap alján ott a 'Ment' gomb). A másik vége a skálának, amit mikrointerakciós oldalaknak szoktak nevezni, ahol minden egyes kis oldalrészlet teljesen saját életet él és reagál a felhasználó műveleteire. Ezek a gombok, listák, csúszkák egymásra hatnak és a szerverrel pici kis csomagokkal kommunikálnak. Ezeken a vezérlő és kijelző darabkákon kívül nem jellemző, hogy szükség lenne egy olyan nagy Ment gombra. Az egész MVC keretrendszer és az általa egyszerűen, kevés gyakorlattal megvalósítható alkalmazás, valahol középúton található. Természetesen meg lehet valósítani mikrointerakciós oldalak kezelését is, de az ilyen esetekre a célszerűség jegyében már megszületetett az ASP.NET WebAPI és számos JS keretrendszer.
3.2. A Modell Kezdjük az M-el, talán azért is mert ez a legegyszerűbb. Végül is a modell nem más, mint összetartozó adatok halmaza. Leggyakrabban egy szimpla osztály más esetben egy lista, megint más esetben egy olyan osztály, aminek a tulajdonságai listák, vagy más osztályok. Lehet egy integer vagy akár egy egész objektum gráf. A modell az adatspecifikációja annak a kapcsolatnak, ami az MVC keretrendszer, a View és a Controller között végbemegy. Konkrétabban, ha azt szeretnénk, hogy a felhasználónak megjelenjenek a saját profil adatai, akkor definiálunk egy nevet, email címet, születési dátumot tartalmazó osztályt és ezt példányosítjuk a kontroller action metódusában, majd átadjuk a View-nak. public class Profile { public string Name { get; set; } public string Email { get; set; } public DateTime BirthDate { get; set; } }
1. példakód
Nagyon fontos a definiált tulajdonságok (propertyk) jó elnevezése. Ugyanis durva közelítéssel, de a folyamat végén a generált HTML elemek elnevezései, azonosítói ebből fognak képződni. Az osztály elnevezése nem kötött, de sokszor szokták kiegészíteni a ’Model’ utótaggal, így a példában lehetett volna ProfileModel is. Ez az elnevezési konvenció végigkíséri az MVC hármast. Érdemes követni, mert így jól áttekinthető kódot kapunk. Például a felhasználói profilkezelés esetében rögtön tudjuk, hogy mely MVC elemek tartoznak össze, ha így nevezzük el: ProfileModel, ProfileController és Profile mappa a View-k számára. Előre szólok, mert később zavaró lehet, hogy a kisebb demóalkalmazásoknál gyakran előfordul, hogy nincs is modell definiálva a kontroller actionje és a View számára. Lehetséges olyan helyzet is, hogy a "nagybetűs" modell nem más, mint egy nagy semmi, egy null, mivel vannak más módok is, hogy adatokat adjunk át a View-nak (pl. ViewBag).
3.2 Első megközelítés - A Modell
1-22
A View kódjában kiaknázhatunk olyan HTML-t generáló metódusokat, amelyek az modellosztályunkon vagy annak tulajdonságain definiált metainformációkat is képesek értelmezni és nagyszerűen felhasználni. Ezeket a metainformációkat természetesen attribútumokban (attribute) tudjuk leírni a .Net lehetőségei miatt. A tulajdonságok metainformációi leginkább a validációra szoktak vonatkozni, de lehetséges befolyásolni a megjelenítést, a szerkeszthetőséget és sok más jellemzőt. Kicsit kidekorálva az előző osztálydefiníciót: public class Profile { [Required] [Display(Name = "Felhasználó név")] [StringLength(100, ErrorMessage = "A {0} legalább {2} karakter hosszúnak kell lennie.", MinimumLength = 6)] public string Name { get; set; } [Display(Name = "Email cím")] public string Email { get; set; } [Required] [Display(Name = "Születési idő")] [UIHint("Birthday")] public DateTime BirthDate { get; set; } }
2. példakód
Egy lehetséges eredményt mutat a következő ábra, miután megnyomtam a Save gombot: Látható az attribútumok hatása, amiknek az elnevezése elég beszédes. Meg tudjuk határozni a beviteli mező felett megjelenő címke tartalmát (DisplayAttribute()). Előírhatjuk, hogy a mező kitöltése 2. ábra 1 kötelező (RequiredAttribute), vagy a beírható karakterek számát korlátozhatjuk (StringLengthAttribute). Sőt azt is közölhetjük, hogy a születési időt ne csak egy textboxban jelenítse meg, hanem egy általunk megírt megjelenítési formában, aminek itt most nem látszik a hatása. Az ilyen attribútumokat a System.ComponentModel.DataAnnotations névtér tartalmazza, nem kötődnek az MVC-hez, más technológiákban is kihasználhatjuk, esetleg onnan is ismerős lehet. A születési idő Required attribútuma nincs jól felparaméterezve így kissé furcsa az alapértelmezett hibaüzenet, de hasonlóan a StringLength-nél alkalmazott lehetőséghez itt is használható lenne az ErrorMessage tulajdonság magyarra fordított tartalmának megadása. Jogos kérdés, hogy van az, hogy az adatmegjelenítést a modellben szabályozzuk és nem a View-ban, ahogy az tiszta lenne (pl. a fenti példában az Display attribútummal)? Érdemes azt is szem előtt tartani, mielőtt a modellt és a View-t elsőre szoros párnak gondolnánk, hogy nem vagyunk korlátozva abban, hogy egy modellt több View-hoz is felhasználjunk. Képzeljük el, hogy van két View a fenti Profile modellhez. Az egyik megjeleníti a nevet és az email címet, míg a másik mind a három adatot. Előnyös, ha a megjelenítést és a modellvalidációt egy közös helyen definiáljuk ilyen esetben. Ezért célszerű létrehozni egy univerzálisabb adatmodellt ahelyett, hogy minden egyes View-ban újradefiniálnánk a megjelenítési szabályokat.
3.3 Első megközelítés - A View
1-23
További érv is szól a jól definiált modell mellett. A felhasználói űrlapot (form) tartalmazó weboldalt legalább két esetben is kezelni kell. Az első, amikor elkészíttetjük az MVC-vel az üres űrlapot (benne a validációs előírásokkal, display nevekkel, esetleg előre kitöltött mezőkkel). A második, amikor feldolgozzuk a felhasználótól érkező kitöltött űrlapot. Ebben is nagy segítségünkre lesz az MVC, ugyanis a kitöltött űrlapot (aminek az adattartalmát előzőleg a modell alapján definiáltuk) szintén ugyanolyan típusú kitöltött modellben kaphatjuk vissza, ha akarjuk. Ráadásul az adathelyesség ellenőrzését, és a validációt is egyszerűen elvégzi helyettünk a rendszer. Egy ilyen metódus így szokott kinézni a bemeneti paraméterként kapott feltöltött modellel: public ActionResult Edit(Profile inputmodel)
Ezt az óriási előnyt, automatikusan a model binder nevű okos mechanizmus szolgáltatja. Összefoglalva, a modell minimálisan nem más, mint egy üzenetcsomag és a csomagjellemzők újrafelhasználható definíciója.
3.3. A View A View egy sablon, vagy más szóval egy template. Template-eket akkor szoktak használni, amikor a generálandó kimenet változatlan és változó tartalmú szakaszokból épül fel. Mint pl. egy körlevél, ahol csak a címzett megszólítása és neve tér el, de a levél törzse egyforma (statikus) minden kiküldendő levélben. Ilyenkor a címzett nevét valamilyen markerrel, tokennel, mezőnévvel helyettesítjük, ami a levél generálásának pillanatában kicserélődik a konkrét névre. Tisztelt [CÍMZETTNEVE]! Tisztelt Claude Debussy! A View-ban a statikus szakaszok HTML nyelvi elemekből épülnek fel. A dinamikus adatok, nos… a bőség zavarában vagyunk. Ugyanis két sablon nyelvet is kapunk egyszerre az MVC framework-el a 3-as verzió óta, és ezt a kételemű listát is bővíthetjük, ha nekünk nem megfelelőek. - Az első az ASP.NET Web Forms hagyományos <% %> markerek közé zárt kódnyelve, ami egy régi örökség. Az első MVC framework kiadások (1. és 2.) csak ezt értették. Ez főként azoknak lesz megfelelő, akik járatosak az ASP.NET Web Forms szintaxisában. Ebben a példában egy egyszerűsített View sablon látható ezzel a szintaxissal: <%@ Page Language="C#" Inherits="System.Web.Mvc.ViewPage<MvcApplication1.Models.Profile>" %> Profile ASP <% using (Html.BeginForm()) { %>
A fenti View sablon csak a Name property megjelenítését tartalmazza a Profile modellből. Az email és a születési idő az áttekinthetőség kedvéért nincs benne. Az ilyen tartalmat természetesen .aspx kiterjesztésű fájlba kell menteni. A sablonban szabványos HTML oldalelemekbe vannak beágyazva a dinamikus szakaszok, <% %> jelekkel határolva. A dinamikus szakaszokba C# kódot lehet írni (vagy VB.Net-et, ha valaki azt preferálja). Az első sorban levő Page direktíva Inherits attribútuma azt határozza meg, hogy a View milyen típusú. Ebben az esetben egy generikus ViewPage, aminek a generikus paramétere a modellünk típusa, amit a 2. példakódban definiáltunk. Érdemes megfigyelni, hogy az MVC aspx fájljának a hagyományos értelembe vett 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 kódblokkokat @ jel (malacfarok, ezt nem viszi a borotva ) vezeti fel, a kód blokk végét nem kell külön jelölni. Ez persze furcsa elsőre, hisz az (X)HTML és az Web Forms <% %>formátuma is a nyitó és záró tagek koncepcióját használja. Ennél a nyelvnél nincsenek lezáró markerek. A sor végét "kitalálja". Az előbbi aspx template Razor nyelven, fele annyi kódjelölővel, sárgával kiemelve: @model MvcApplication1.Models.Profile ProfileRazor @using (Html.BeginForm()) { @Html.ValidationSummary(true)
A fájl kiterjesztése .cshtml vagy .vbhtml, ami a View-ba ágyazott kód nyelvére utal. Aki PHP programozási tapasztalattal rendelkezik, annak nem lesz meglepő a formátum, hisz abban a világban tucatjára állnak rendelkezésre ehhez hasonló template nyelvek (Smarty, Xtemplate, Blitz,…). Az ASP.NET gyakorlóinak kicsit idegen lehet, legalábbis nekem az volt elsőre, de néhány próbálkozás után rá kellett jönnöm, hogy jobban áttekinthető View-t lehet vele definiálni, ami a fenti példáknál összetettebb View készítésénél válik igazán hasznossá. Elég csak az első sort megnézni, ami szintén a modell típusát határozza meg: @model MvcApplication1.Models.Profile és összevetni a Web Forms megfelelőjével, amit az előbb néztünk. A későbbiekben a példakódok ebben a Razor stílusban lesznek bemutatva. Részletesen a 6.6.1 alfejezet mutatja be a használatát és a rejtelmeit.
3.4 Első megközelítés - A Kontroller
1-25
3.4. A Kontroller Következik a C összetevő, ami a Controller ősből származtatandó saját osztály, aminek az osztályneve a 'Controller' szóval kell végződnie. Íme, rögtön egy példa: public class HomeController : Controller { public ActionResult Index() { ViewBag.Message = "Modify this template to jump-start your ASP.NET MVC application."; return View(); } public ActionResult About() { ViewBag.Message = "Your app description page."; return View(); } public ActionResult Contact() { ViewBag.Message = "Your contact page."; var contactModel = new MvcApplication1.Models.ContactModell(); contactModel.FirstName = "Kapcsolat"; contactModel.LastName = "Tartó Gizi"; contactModel.PhoneNumber = "0690000000"; return View(contactModel); } public ActionResult Profile() { return View(new Profile()); } }
5. példakód
Mint már említettem a kontroller tartalmazza a request kiszolgálásához szükséges kódunkat. A request magában foglalja azt az URL-t, ami alapján a böngésző a válaszban a tartalmat várja. Például, ha az URL így néz ki: http://localhost/Home/Contact, akkor az MVC framework, a (route) konfigurációnak megfelelően a domain név utáni részt (/Home/Contact) megvizsgálva úgy fogja értelmezni, hogy HomeController Contact() paraméter nélküli metódusát kell meghívnia. A Contact() action metódus visszatérési értéke egy ActionResult-ból származó osztály, amit a Controller ősosztályban megvalósított View() metódus tud előállítani. Névkonvenció alapján dől el, hogy melyik View template lesz az, ami sablonként fog szolgálni a dinamikus HTML tartalom előállításához. Jelen esetben a View az Action nevével egyező „Contact.cshtml” fájl lesz. Az elérési útja a projekt gyökeréből: Views/Home/Contact.cshtml. Ezt nagyon fontos jól megérteni. Itt nem egy fájl megcímzéséről van szó, mint az ASP.NET Web 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: http://localhost/User/Profile/15 Az előbbi példakódban már látszik, hogy a Contact() metódusban létre van hozva egy ContactModell példány, aminek a tulajdonságai beállításra kerülnek, és ez a modell paraméterként kerül átadásra a View() metódus számára. A Profile() metódus pedig elégséges a Profile modell példányosításával arra, hogy a 1. ábra szerinti weboldalt kapjuk. A kontroller az oldal generálás lelke. Olyan előfordulhat, hogy nincs modell definiálva, ezen kívül olyan is, hogy View sem, de kontroller és action nélkül nem megy. Az alapképlet ennyi. Ugye, hogy nem bonyolult?
3.5 Első megközelítés - Próbáljuk ki!
1-26
3.5. Próbáljuk ki! Ennyi alapelmélet és bevezető után szükséges, hogy a gyakorlati síkra ugorjunk át. Felteszem, hogy egy Visual Studio-t sikerült beszerezni, a fejlesztési környezet is kész. Akkor, most hozzunk létre egy MVC alkalmazást a File menu, New project… menüpontjával! A projekt sablonok közül válasszuk ki az ASP.NET MVC 4 Web Application-t és adjunk egy nevet a projektnek. (FirstMVCApp). A lista felett van egy .Net Framework 4 –en álló keretrendszer választó. Ezzel lehet beállítani, hogy a projektünk melyik .Net verzió szerint épüljön fel.
3. ábra
OK gomb megnyomása után válasszuk ki a konkrét projekt template-et.
4. ábra
A projekt template-ek által létrehozható alkalmazásváltozatok:
Empty: Azért teljesen nem üres. Tartalmazza az assembly referenciákat, egy global.asax fájlt és egy minimális elrendezési beállítást. Basic: Egy kicsivel több, mert itt a konvencionális projekt struktúra is meg fog jelenni.
3.5 Első megközelítés - Próbáljuk ki!
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.
1-27
3.6 Első megközelítés - Az alkalmazás felépítése
1-28
3.6. Az alkalmazás felépítése Nézzük meg milyen projektet hozott létre az előbbi varázslás! A projekt egy mappákkal jól strukturált projektből áll. Fentről lefelé haladva nézzük meg ezeket és a céljukat. App_Data –t adatbázis fájlok tárolására lehet használni. Most még nincs tartalma. App_Start beállító kódok gyűjteménye, amik az alkalmazás első indulásakor kapnak szerepet. Content-ben jellemzően statikus, nem fordítandó fájlok vannak. Képi elemek, CSS fájlok, amiket minden további nélkül le lehet tölteni a böngészőbe. A Site.css a stílus sablon. Controllers tartalmazza a kontrollerek osztályait. Filters-be kerülhetnek az Actionök viselkedését, elérhetőségét szabályzó attribútum definíciók.
7. ábra 6.
Images hasonlóan a Content mappához, statikus képfájlokat szokott tartalmazni.
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ó.
3.7 Első megközelítés - Új Model, View, Controller hozzáadása
3.7. Új Model, View, Controller hozzáadása A projektvarázslóval legyártott projektet megnéztük és láttuk, hogy egy azonnal bővíthető minialkalmazást kaptunk, készre sütve. Az MVC a Visual Studio-val együttműködve további könnyed lehetőségekkel támogatja, hogy az alkalmazásunkat új oldalakkal bővítsük. Célszerű azzal folytatni, hogy meghatározzuk, hogy milyen adatot szeretnénk megjeleníteni. Ehhez készítsünk egy tetszőleges modellosztályt néhány propertyvel, és tegyük a Models mappába. Annyit érdemes már ilyenkor eldönteni, hogy milyen elnevezést használunk az oldalunk számára, mert ezt a nevet célszerű végigvezetni a kontroller és a View elnevezésén is. Ez az elnevezés most a 'First' lesz. Modellt a hagyományos módon adhatunk hozzá. A modell neve most 'FirstModel' lesz.
public class FirstModel { public int Id { get; set; } public string FullName { get; set; } public string Address { get; set; } }
Ha ezzel megvagyunk, mozgassuk az egeret a Controllers mappára és kérjünk egy helyi menüt. Bökjünk a menü Add->Controller… elemére .
A dialógus ablakban töltsük ki a kontroller nevét, ügyelve arra, hogy a választott név után szerepeljen a Controller utótag. A 'Template' legördülő listát is a képen látható módon állítsuk be.
8. ábra
1-29
3.7 Első megközelítés - Új Model, View, Controller hozzáadása 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.
1-30
3.7 Első megközelítés - Új Model, View, Controller hozzáadása 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. 9. ábra
Nézzünk rá a kész View első sorára: @model IEnumerable 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.
1-31
3.7 Első megközelítés - Új Model, View, Controller hozzáadása A vastagon szedett sor lenne az, a többi három sor már ott volt:
Az ActionLink első paramétere lesz a link felirata, a második az Action neve, a harmadik a kontroller neve (FirstController). Most indulhat az első menet. A projekt fordítása utáni futtatáskor a böngészőben megjelenik az új főmenü. Az új 'Első próba' menüpontra kattintva az Index által szolgáltatott listanézet ez lesz:
A sorok mellett ott vannak az Edit, Details és Delete linkek, amik ténylegesen a First kontroller azonos nevű actionjeire mutatnak. Ha megnyomjuk valamelyiket csak egy sárga hibaüzenetet kapunk (YSOD14), hiszen az actionök ugyan kész vannak, de a hozzájuk kapcsolódó View-k még nem. A további actionök végén levő View() metódushívásokra elkészíthetjük a hozzájuk tartozó View-kat, az Add View… menüponttal. Az egyetlen eltérés, hogy a „Scaffold template” legördülő listából hozzá kell passzoltatni a megfelelő View generátor templatet az action funkciójához. A Details actionhöz a Details template illik, ahogy a képen is látható: A View name és minden más is jól van kitöltve. Ami változik az csak a template actionrő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
1-32
3.7 Első megközelítés - Új Model, View, Controller hozzáadása 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)
@Html.ActionLink("Back to List", "Index")
Átírva:
@Html.ActionLink("Vissza a listához", "Index")
Hasonló módon átírhatjuk a lista utolsó oszlopainak (Edit,Details,Delete) a feliratát is: @foreach (var item in Model) {
@Html.DisplayFor(modelItem => item.FullName)
1-33
3.7 Első megközelítés - Új Model, View, Controller hozzáadása
@Html.DisplayFor(modelItem => item.Address)
@Html.ActionLink("Szerkesztés", "Edit", new { id = item.Id }) | @Html.ActionLink("Részletek", "Details", new { id = item.Id }) | @Html.ActionLink("Törlés", "Delete", new { id = item.Id })
}
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: Szerkesztés Nem csak a kontroller és action neve kerül bele az URL-be, hanem a végére az "1" is, mert ez felel meg az id propertynek (hogy miért, azt majd hamarosan meglátjuk). Az ehhez az URL-hez tartozó action paraméterlistájában szintén ott szerepel az id: public ActionResult Edit(int id)
Emiatt az Id paraméterében megjelenik majd az 1-es érték, ha a 'Szerkesztés' linkre kattintunk. Illetve a 2-es érték, ha az alatta levő sor 'Szerkesztés' linkjére kattintunk, mert annál az id értéke 2 lesz. Ahhoz, hogy a szerkesztés oldalon végzett módosításokat át tudjuk venni típusosan, csak annyit kell tenni, hogy a másik (HttpPost attribútumos) Edit action paraméterét kicsit átírjuk ilyenről: [HttpPost] public ActionResult Edit(int id, FormCollection collection)
Ilyenre: [HttpPost] public ActionResult Edit(int id, Models.FirstModel model)
Mostantól, ha megváltoztatom például a 'Teljes név' beviteli mező tartalmát, és mentem az oldalt, akkor az Edit actionbe megérkezik a módosított adatokat tartalmazó feltöltött FirstModel példány.
Ezt azért tudjuk így megtenni, mert a View létrehozásakor (a View dialógusablakban) beállítottuk a modell típusát (FirstModel). Mivel így állítottuk be, a létrehozott View is típusosan lett legenerálva. Idemásoltam az Edit.cshtml fájl idevonatkozó részletét. @using (Html.BeginForm()) { }
A vastagon kiemelt sorok hozzák létre a form input mezőit. Ezek neve meg fog egyezni a property nevével: FirstModel.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 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.
1-35
3.8 Első megközelítés - Próbáljuk ki menthető adatokkal!
3.8. Próbáljuk ki menthető adatokkal! Most már tudjuk, hogyan kell elkezdeni az építkezést MVC-ben, nézzünk meg egy életszerűbb példasort, amiben a modellünkben levő adatokat el is tárolhatjuk. Az MVC projektünkben a referenciák között megtaláljuk az Entity Framework (EF) 4.4-es verzióját. Ez egy ORM mapper, ami az adatbázis tábla (és egyéb) sémákat összerendeli a normál .Net osztályokkal. Ezzel objektumorientáltan, típusosan tudjuk a táblaadatokat kezelni. Az un. code first megközelítés lehetővé teszi, hogy ne kelljen adatbázissal, SQL-el, XML modellekkel, tervezői felülettel, séma mappeléssel foglalkozni. Egyszerűen csak létrehozzuk az osztályunkat és minden egyebet rábízunk az EF-re. Ez majd létrehozza az adatbázist, ha még nincs, és a táblákat is az előre definiált osztályaink alapján. Kis MVC alkalmazásoknál teljesen járható megoldás, hogy az EF 'code first' osztályok legyenek az MVC modellek is egyben. Ezzel jó sok munkától meg tudjuk magunkat kímélni. A következő példákban is így fogunk eljárni. Hozzunk létre egy új MVC internet projektet a 3. ábra szerint.
Hozzunk létre egy új modellt A cél az lesz, hogy egy szimpla névjegykártya regisztert készítsünk el. Először szükségünk van a jól definiált modellekre (code first!). A modellpropertyket el kell látni minden olyan attribútummal, ami jelezni tudja, hogy milyen szabályok szerint fogjuk használni azokat. Ez két irányba is jelez: MVC felé, hogy milyen validációs szabályokat alkalmazzon a propertyhez kapcsolódó beviteli mezőkre. Illetve az EF felé is, hogy milyen mezőtípusokat és mezőhosszakat használjon az adatbázistáblák létrehozásakor. Nézzük meg ezt a FullName modellproperty definíciót példaként: [Display(Name = "Teljes név")] [StringLength(100)] [Required] public string FullName { get; set; }
A neve alapján létre fog jönni egy FullName táblamező. A StringLength(100) miatt az MVC nem fogja engedni, hogy 100 karakternél hosszabb szöveget adjunk meg. Ami jó is lesz, mert az EF a FullName táblamezőt nvarchar(100) típusúra fogja beállítani ez alapján. Így többet nem is fogunk tudni tárolni benne. A Required az MVC-nek azt üzeni, hogy követelje meg a felhasználótól a FullName beviteli mező kitöltését a böngészőben. Az EF ezt az attribútumot úgy fogja értelmezni, hogy a FullName táblamezőt 'NOT NULL' megkötéssel kell létrehoznia. Ezek után már érthetőek lesznek a modellek: public class CardRegister { public CardRegister() { PhoneNumbers = new List(); } 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; }
1-36
3.8 Első megközelítés - Próbáljuk ki menthető adatokkal! [Display(Name = "Beosztás")] [StringLength(150)] public string Position { get; set; } //Navigation property public virtual ICollection 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 CardRegisters { get; set; } public DbSet 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.
1-37
3.8 Első megközelítés - Próbáljuk ki menthető adatokkal! A nagy kontrollervarázsló A sikeres fordítás után jön a modell alapú alkalmazásgenerálás15: Hozzunk létre egy új kontrollert a helyi menüvel, a Controllers mappán:
A bal oldali ábra szerint állítsuk be a mezőket és adjunk meg egy konzekvens nevet a kontroller számára. Amire figyelni kéne, az a 'Template' lista pontos beállítása, mert több hasonló nevű eleme van. Az 'Add' gombbal el fog készülni a kontroller, és a kontroller action metódusai számára az összes View fájl is.
Tulajdonképpen ezzel kész is vagyunk. A CardReader/Index oldalt meg is nyithatjuk és működni fognak a listázó, szerkesztő, törlő funkciók. Még talán érdemes a _Layout.cshtml-ben a felső menüpontokhoz hozzáadni az új Index oldalra mutató linket:
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.
1-38
3.8 Első megközelítés - Próbáljuk ki menthető adatokkal! A megjelenő oldalon a beviteli mezők érvényre juttatják a hozzájuk kapcsolt validációs szabályokat. A FullName ('Teljes név') property a Required attribútum miatt kötelezően kitöltendő.
A Title (Megszólítás) mezőbe nem lehet 10 karakternél többet írni, mert a StringLength(10) attribútum ezt határozta meg. Ha értelmes adatokat adunk meg, a 'Create' gombra kattintva elmentődnek az adatbázis tábla sor mezőiben, és létrejön az új névjegykártya.
A 'Details' linkkel megnyílik az oldal, ami nagyon össze van esve. Sebaj. Nyissuk meg a Content/site.css fájlt, ami az alkalmazás stílusdefiníciója, és írjuk be a végére ezeket CSS osztálydefiníciókat. Az eredményt a jobb alsó kép mutatja. .display-label { display: inline-block; width: 15%; background-color: #ddd; margin-bottom: 4px; padding-left: 5px; } .display-field { display: inline-block; width: 80%; } .phones { border: 1px dotted #ddd; padding: 10px; }
Tudunk létrehozni, menteni, törölni CardRegister elemeket. Most vessünk egy pillantást a projektre és az App_Data mappa alatt megtalálhatjuk az időközben létrejött háttéradatbázist. A 'Show All files' gombbal tudjuk megjeleníteni, mert nincs a projekthez csatolva. (felső képsáv) Ha duplán kattintunk az .mdf fájlra, megnyílik a Server Explorer és benne megtaláljuk a létrejött táblákat.
1-39
3.8 Első megközelítés - Próbáljuk ki menthető adatokkal! 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:
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 sorok) @model IEnumerable
Index
@Html.ActionLink("Create New", "Create", new {cardid = })
1-40
3.8 Első megközelítés - Próbáljuk ki menthető adatokkal! @Html.DisplayNameFor(model => model.Number)
@Html.ActionLink("Edit", "Edit", "PhoneNumber", new { id=item.Id}, null) | @Html.ActionLink("Details", "Details", "PhoneNumber", new { id=item.Id }, null) | @Html.ActionLink("Delete", "Delete", "PhoneNumber", new { id=item.Id }, null)
}
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ó után, részleges View-ként hivatkozzunk az előbbi fájlra (partial View-ra):
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
1-41
3.8 Első megközelítés - Próbáljuk ki menthető adatokkal! névjegykártyának a Details nézetéből idehivatkoztunk. A View-nak átadunk egy nagyjából üres modellt csak a CardRegisterId-t töltjük fel. Amikor a kitöltött telefonszámot tartalmazó form az alábbi Create az actionhöz küldi a mezői tartalmát, a form mezői alapján feltöltött PhoneNumber objektumot kapjuk meg metódusparaméterként. Ez az objektum olyan állapotban van, hogy minden további nélkül menthetjük is az adatbázisba. (Add(…) és SaveChanges() metódushívások). [HttpPost] [ValidateAntiForgeryToken] public ActionResult Create(PhoneNumber phonenumber) { if (ModelState.IsValid) { db.PhoneNumbers.Add(phonenumber); db.SaveChanges(); return RedirectToAction("Details", "CardRegister", new { id = phonenumber.CardRegisterId }); return RedirectToAction("Index"); } ViewBag.CardRegisterId = new SelectList(db.CardRegisters, "Id", "FullName", phonenumber.CardRegisterId); return View(phonenumber); }
Ebben az actionben csak annyit érdemes változtatni, hogy ne a telefonszámok listájához ugorjon át, ha jól töltöttük ki a formot, hanem a telefonszámhoz tartozó névjegykártya nézet oldalára. Vissza, ahonnan elindultunk az 'Új telefonszám' linkkel. Ezt oldja meg a vastagon kiemelt RedirectAction a paramétereivel. A 'RedirectToAction' sort érdemes kicserélni az Edit és a Delete actionökben is. Hogy még teljesebb legyen a felhasználói élmény. Nézzük meg a Views/PhoneNumber/ alatti View fájlok végét. Mindegyikben ott lesz egy navigációs link, ami visszamutat a telefonszámokat listázó PhoneNumber/Index oldalra ('Back to List'). Ha ezeket kicseréljük az alábbi példa alapján, akkor az új link szintén a telefonszámhoz tartozó névjegykártya oldalra fog visszavezetni. @Html.ActionLink("Edit", "Edit", new { id=Model.Id }) | @Html.ActionLink("Vissza a névjegykártyához", "Details","CardRegister", new {id = Model.CardRegisterId}, null)
Azaz vissza a CardRegister kontroller Details action metódusához úgy, hogy a metódus 'id' paramétere legyen feltöltve a Model.CardRegisterId értékével. Ez a néhány oldalas bemutatót figyelem felkeltésnek szántam. Remélem sikerült megmutatni, hogy az MVC nagyon jól együtt tud működni külső adatforrással, kiváltképp az Entity Framework-kel.
1-42
3.9 Első megközelítés - A projekt beállításai
3.9. A projekt beállításai Érdemes megnézni, hogy milyen alapértelmezett beállításokkal jönnek létre az új MVC projektek. Ha a Solution Explorerben a projekt nevét kiválasztjuk és nyomunk egy Ctrl+Enter kombót vagy a jobb egér gombbal a helyi menüből a ’Properties’ pontot választjuk, megjelennek a projekt tulajdonságai. Nézzük meg a lényeges beállítási lehetőségeket. Ezekről már most jó, ha tudomást szerzünk. Az első az Application fül tartalma:
Az ’Assembly name’ határozza meg, hogy a lefordított kódunk milyen nevű .dll kiterjesztésű fájlba fog kerülni. A ’Target framework’ a futtatáshoz szükséges .Net keretrendszer verzióját határozza meg. A lefordított kód legalább ilyen verziószámú .Net környezetben fog tudni futni. Ha ezt megváltoztatjuk a Visual Studio a referált .Net dll-eket is megpróbálja aktualizálni az új beállításhoz. Ez néha sikerül, néha nem, emiatt az új projekt létrehozásakor célszerű meggondoltan beállítani a 3. ábra felső részén levő .Net verziót. Az ’Output type’ ’Class Library’, mert az MVC alkalmazás futásához ez kell. A Build fül alatt csak az ’Output path’-t emelném ki.
Ez a ’bin\’ azt jelenti, hogy a projektünk gyökerében létre fog jönni egy bin almappa és a lefordított kódok .dll fájljai ebbe fognak kerülni. Egy új VS projekt esetén ide másolódnak az MVC futásához szükséges további dll-ek is. A fejlesztés során a leglényegesebb beállítások a Web fül oldalán vannak. A ’Specific Page’ beállítása alapértelmezetten üres. Fontos tudni, hogy itt meg lehet adni kezdő oldalt a projektünkben levő fájlok és elérési utak közül, ami akkor indul el, amikor az alkalmazásunkat a Visual Studio-ból indítjuk. Az MVC nem fájl alapon szolgálja ki a kéréseket, ezért ide ne írjunk olyan elérési utat, aminek a végén egy fájl található, mert ez a beállítás az ASP.NET Web Forms fejlesztés esetén hasznos. Az MVC-ben mindig kontrollert és actiont kell megcéloznia az URL-nek. Ilyet lehet beírni ide: Home/About (Nem kell a Home elé / jel). A másik módszer arra, ha azt szeretnénk, hogy a fejlesztés során ne mindig a kezdő oldal jelenjen meg és innen kelljen továbbnavigálni a fejlesztés/tesztelés alatt levő oldalra, akkor válasszuk a ’Start URL’-t és írjuk be a teljes URL-t pl.: http://localhost:18005/Home/About
1-43
3.10 Első megközelítés - A MVC komponenseinek működési ciklusa A Servers szekcióban beállítható a fejlesztés során használandó webszerver és ennek a legfontosabb paraméterei. A Visual Studio 2010-ig az alapértelmezett beállítás a ’Use Visual Studio Development Server’ volt, emellett használhattuk még az operációs rendszerre telepített IIS webszervert 16 is. Ezen az ábrán a VS 2012-es beállításai láthatóak. Az alapértelmezett most a VS-val feltelepült ’Local IIS Web server’, ami valójában egy IIS Express webszerver. Ez sokkal közelebb van minden jellemzőjében a teljes értékű IIS webszerverhez. Emiatt az alkalmazásunkat is jobban tudjuk tesztelni, mintha a VS belső development server-ét használnánk. Várhatóan kevesebb kellemetlen meglepetésben lesz részünk, 10. ábra amikor majd a kész alkalmazásunkat az éles IIS szerverre telepítjük. A ’Project Url’ a legfontosabb beállítás, ez határozza meg, hogy futásidőben a böngészőben milyen URL-t kell megadni, hogy a futó webalkalmazásunkat címezze meg. A képen látható ’http://localhost:18005/’ beállítás volt az alapértéke a ’Start Url’ beállításnak. Az ’Apply server settings to all users’ egy érdekes beállítási lehetőség, ha többen dolgozunk egy projekten. Ha kivesszük a pipát, akkor az előzőleg tárgyalt beállítások a projekt mappa [Projektnév].csproj.user fájlban fognak tárolódni és a projekt fájl közösen használható lesz, de a web server beállítások, különösképpen a port szám, viszont felhasználónként egyedi lesz. Ez a leghasznosabb, abban a ritka helyzetben, ha egy fejlesztői gépen egyszerre (pl. terminál szerverrel) többen fejlesztenek. Így nem lesz portütközés, mert több fejlesztői webszerver tud futni más és más porton.
3.10. A MVC komponenseinek működési ciklusa Nézzük végig nagyvonalakban hogy mi történik, ha az az előbb összeállított alkalmazásunk elindul.
16
A projekt gyökerében levő global.asax-ban definiált MvcApplication-ünk Application_Start() metódusa lefut és beállítja az MVC framework általunk meghatározott jellemzőit. A következő lépésben a csak domain nevet és portszámot tartalmazó URL (pl.: http://localhost:18005/) alapján úgy dönt, hogy példányosítja a Controllers/HomeController.cs-ben található osztályt és elindítja az Index() metódusát. Az Index() metódus még szinte semmit sem tartalmaz, csak meghívja a kontroller View() metódusát, aminek a visszatérési értéke, egyben az Index() metódus visszatérési értéke is lesz. Az action metódus futása után betöltődik a Views/Home/Index.cshtml fájl és az MVC értelmezi a tartalmát és elkészíti belőle a HTML markupot. A HTML tartalmat mint választ, visszaküldi a böngészőnek. Mikor az 'Első próba' linkre rákattintunk, egy teljesen új MVC ciklus indul el. Az eltérés csak annyi, hogy az URL-ben megjelenik majd a /First (http://localhost:18005/First), ami a FirstControllert jelenti. Az Index-et is odaírhatnánk: First/Index, de nem kell, mert az Index-et odaérti az MVC, mert ez az alapértelmezett. Az Index actionben a modellt példányosítva adjuk át a View-nak, ami pedig felhasználja azt, és az IEnumerable felsorolás elemein végiglépkedve listasorokat készít belőlük.
Internet Information Services. A Windows operációs rendszereken futó webkiszolgáló.
1-44
3.10 Első megközelítés - A MVC komponenseinek működési ciklusa
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.
1-45
4.1 Modell - Modell és tartalom
1-46
4. Modell Mikor ez a három fő fejezet (Modell-Kontroller-View) már 1/3 részben kész volt, elkezdtem gondolkozni, hogy vajon jól vannak-e sorrendbe rakva és bizony gondban voltam. Ugyanis ezek a témák nagyon erősen összefüggenek. Nehéz úgy részletesen beszélni az action metódusokról és paramétereikről, hogy előtte ne tárgyaljuk a route beállításokat, amit viszont nehéz elsőre részletesen bemutatni, ha az action metódusokról előtte nem beszéltem. Hasonlóan van ez a modell és az action viszonylatában is. Emiatt ezeknek a fejezeteknek az elolvasásához azt tudom tanácsolni, hogyha elsőre nem világos valami, akkor csak haladjunk tovább, és ha a három fejezet végén még mindig sok a kérdés, akkor még egyszer érdemes átolvasni. [Ha ezek után sem, akkor lehet, hogy le kéne fordítanom a könyvet magyarra…]
4.1. Modell és tartalom Az 3.2 fejezetben néhány dolgot megmutattam a modell szerepéről, de azóta kicsit nagyobb rálátással bírunk az egészre. Mint említettem a modell elődleges célja nem más, minthogy egy típusos adathordozó (osztály) legyen. Emiatt, a modellre nem annyira jellemző néhány objektum orientált tervezési ajánlás. Olyanokra gondolok, hogy az objektumnak csak egy oka legyen a megváltozásra, csak egy felelőségi köre legyen, stb. Elsődlegesen ez egy típusos tároló, minden más szempont csak ez után következik. A modell használatával, tervezésével kapcsolatban összegyűjtöttem néhány ajánlást és megfontolandó szempontot. Először is lássuk meg két pontban, hogy pontosan hol jelenik meg a modell.
A View számára adatok tárolása: Az általános szituáció, amikor a View által létre szeretnénk hozni a dinamikus oldalt. A képen a Home modell feladata, hogy tároló helyet szolgáltasson a View-n megjelenő mezők adatai számára. Ha szeretnénk egy ’felhasználónév’ mezőt megjeleníteni a HTML-ben, akkor a modellben definiálunk egy propertyt hozzá. Ha pedig egy táblázatot vagy egy comboboxot szeretnénk megjeleníteni, akkor a modellben definiálunk egy listát hozzá.
4.1 Modell - Modell és tartalom
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. 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
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 public public public }
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.
Az előbbi formot a következő action metódusnak kéne fogadnia, de a WrongModel típusú inputmodel paraméter mezőinek a kitöltése során Exception fog keletkezni. public ActionResult HnameCollision(WrongModel inputmodel) { return View(model); }
Ezzel az actionnel is probléma lesz. Mind a három string paraméterben a "Name" értéke fog megjelenni, jelen esetben a "Tanuló 1". public ActionResult HnameCollision(string Name, string name, string NAME) { return View(model); }
A problémák abból adódnak, hogy a WrongModel property neveiből dictionary kulcsok lesznek, ahol a kis-nagybetű eltérés nincs figyelembe véve. Természetesen senki sem szokott közel azonos, publikus nevekkel operálni egy osztályon belül, de gondoljunk az öröklésre is. A modell ősosztályában levő tulajdonságokkal se legyen ütköző elnevezés.
4.2. Modell és kód Mivel a modellünk egy osztály, semmi sem gátol minket abban, hogy a modellbe az adatokhoz szorosan kötődő kódokat írjunk. Most csak két megvalósítási irányelvet mutatnék be nagyvonalakban. A valóság nem ennyire szélsőséges és merev, mintha csak két lehetőségünk lenne, hanem inkább ezek keveréke jellemző. Mindenesete egy jól szervezett kódban könnyebb eligazodni.
4.2.1. Viselkedéssel bővített modell Itt arról van szó, hogy a View-ban megjelenő kódot (hamarosan lesz róla szó) minimalizáljuk és amit lehet áthelyezzük a modellbe. A legtöbb View-ban lesznek olyan részek, amikor egy HTML tulajdonság, CSS stílus vagy CSS osztály a modellben definiált egy vagy több property tartalmától függ. Sőt az is tipikus eset, amikor azt akarjuk, hogy egyes szakaszok a View-ban csak bizonyos esetekben legyenek egyáltalán figyelembe véve az oldalgeneráláskor. Erre egy példa, hogy nem érdemes megjeleníteni a felhasználó nevét, amíg nincs bejelentkezve (mivel nincs is elérhető felhasználói profil adat), tehát az ezt előállító View szakasz kimaradhat. Ilyen esetben a megjelenést szabályzó értékeket sokszor (szerintem helytelenül) a View elején egy @{..} blokkban számolják ki, és hoznak létre helyi változókat. Pedig ez nagyon ellentmond a feladatok elszeparálásának elvének. Ráadásul az ilyen kódok csak futásidőben kerülnek lefordításra, tehát ha hibás, az túl későn derül ki. Ilyenkor lehet bevetni ezt a koncepciót, és akkor az adatok és az adatokból számított további értékek is a modellben lesznek.
1-48
4.2 Modell - Modell és kód 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"; } } }
1-49
4.2 Modell - Modell és kód A View kódját mellőzve a felhasználási értelmezésének pszeudo kódja ilyesmi lehet: Ha van szállítási dátum (DeliveryDateAvailable), akkor jelenítsd meg a következő blokkot { Szállítási dátum feiratának és értékének a kiiratása } Ha nincs szállítási dátum és a megrendelés dátuma több mint két nap { Vastagon kiemelve egy szöveg, hogy ’Késedelmes szállítás, azonnal intézkedj!’ } Ha vannak a szállítási út mentén további intézni valók vannak (Arranges) { Listázza az intézni való dolgokat egy táblázatban } Ha az ügyfél VIP { A nevének a kiiratásánál alkalmazd a következő CSS osztályt: ”vipcustomer” } Egyébként { A nevének a kiiratásánál alkalmazd a következő CSS osztályt: ”normalcustomer” }
Ezek a számított értékek az esetek egy jó részében boolean típusúak, tehát csak egy döntés eredményét hordozzák. Igazán hasznos tud lenni ez a megközelítés, ha arra gondolunk, hogy a modellünket több View-val kapcsolatban is használni szeretnénk. Ekkor a megjelenítéssel összefüggő feltételek az egységes modellben értékelődnek ki, emiatt a View-k megjelenési szabályrendszere is egy helyen lesz karbantartható. Példaként képzeljünk el egy egyszerűsített szituációt, amiben adott több View, amelyek azonos modellt használnak, amelyik modellen van egy dátummező definiálva. Egyszer csak az az igény merül fel a felhasználótól, hogy ez a mező pirossal jelenjen meg, ha a dátum értéke régebbi mint a mai nap. Ha ezt a feltételt a modellben értékeljük ki, akkor az összes View-t egy helyről ki tudjuk szolgálni. Igen ezt a pirosítást más módon is meg lehet csinálni, de ha bővül a feltétel, mondjuk azzal, hogy csak akkor kell pirosnak lennie, ha régebbi, mint a mai nap és egy másik mező nincs kitöltve és a Karcsi még nem hagyta jóvá és egyébként még jogom is van kitölteni és ... Gondolom érzékelhető, hogy gyorsan változó, kikristályosodó üzleti igények mellett komoly létjogosultsága van ennek a kiértékelési megközelítésnek.
4.2.2. Üzleti logikával bővített modell Az előző megközelítés nagyjából megáll a megjelenítést szabályzó (általában pár soros) metódusok, propertyk implementálásánál vagy a propertyk attribútumos dekorálásánál. A most tárgyalt megközelítés azt mondja, hogy ha már úgy is valahol implementálni kell a nagybetűs Üzleti Logikát, akkor tegyük bele a modellbe azt is. Még akár bele is férhet a modellbe és akkor egy helyen van minden, ahogy az OOP logikája adja, az adat és a hozzá tartozó feldolgozás is. A kontroller is megszabadulhat a sok kódtól. Azonban óvatosan építsünk ilyen modelleket! A probléma a modellekben implementált kódok függőségei. Tartsuk szem előtt, hogy a modellosztályt az MVC leginkább egy önmagában érvényes adatblokként kezeli és értelmezi. Ha magával hurcol más üzleti logikákat tartalmazó osztályokat, adatlistákat, akkor felesleges inicializálások, vagy éppen inicializálatlan osztályok jöhetnek létre. Ezért kis rendszereknél még elmegy az üzleti logikát, számításokat, workflow-kat hordozó modell. Nagy rendszereknél, ahol a mi (kis) MVC projektünket súlyos szolgáltatások látják el adatokkal és azok végzik az üzleti igény megvalósítását, hát itt azt mondanám, hogy nincs létjogosultsága. Bár minden helyzet egyedi. Ha a távlati tervben szóba jöhet akár csak egy kis gondolatfoszlány formájában is, hogy szolgáltatás alapú architektúrát használjunk,
1-50
4.2 Modell - Modell és kód akkor kerüljük, hogy az MVC modellbe komoly kódot helyezzünk el. Ilyen Service Oriented Architecture17 esetben a kontrollerek kódjára is vonatkozik ez a javaslat. Ha mégis ide tervezzük az üzleti kódokat, akkor készítsünk hozzá valami olyan háttér infrastruktúrát, ami tervezési minták (repository, factory) alapján egységes hidat képez az adatforrás, a modellosztályok és az osztályokon definiált üzleti logika futtatása között, interakcióban az MVC infrastruktúrájával. Másként nagyon kusza kódot fogunk kapni. Összefoglalásként azt tudnám javasolni, hogy a modellt ne terheljük túl kóddal. Egy egyszerű választási sablon lehet, hogy
Ha kéne kódot írni a View-ban, akkor azt tegyük inkább a modellbe, Ha sok lenne a kód a modellben, akkor legyen inkább a kontrollerben, Ha sok lenne a kód a kontrollerben, akkor legyen inkább egy szolgáltatásban vagy egy külön segédosztályban.
Hogy kinek mi a sok kód, az leginkább tapasztalat kérdése. Ezek csak iránymutatások voltak.
4.2.3. A konstruktor probléma Mivel a modell egy osztály, kézenfekvőnek tűnik, hogy a modell kezdeti belső értékeit, a konstruktorból állítsuk be. Nagyon fontos megjegyezni, hogy a modell konstruktorába maximum olyan inicializáló kódot érdemes tenni, ami annyit teljesít, hogy a modell felhasználásakor a null reference exceptionöket el tudjuk kerülni. A felesleges alapbeállításokat is érdemes elkerülni (int típusnak 0 érték, booleannak false, stb). Olyan kódot soha ne tegyünk a paraméter nélküli konstruktorba, ami adatbázisból vagy fájlból adatokat olvas, erőforrásokat nyit meg. Egyébként sem szép, de a modellnél messze kerülendő. Jellemzően a modellben tárolt listboxok és comboboxok elemlistáit szokták így (helytelenül) feltölteni. Az óvatosság oka az, hogy a modellünket az MVC belső infrastruktúrája is tudja példányosítani (model binder) néha feleslegesen is, ami óriási overheadet visz a működésbe, ha ilyenkor terjedelmes és lassú konstruktorkódnak kell lefutnia. Minek töltenénk fel a combobox elemlistáját, ha nem is használjuk fel a beérkező post request esetén? Célszerű a modellt, egy külön beállító metóduson keresztül feltölteni alapadatokkal (Setup, Init) ahogy más hagyományos osztálynál is, vagy fenntartani egy paraméteres konstruktort erre a célra. A framework működéséből következően, a modellt a kimenő HTTP válasszal kapcsolatban kell feltölteni adatokkal, amikor amúgy is a saját kódunk felügyelete alatt van a modell példányosítása és alapbeállítása. Ekkor azt csinálunk vele, amit akarunk. Ugyan ez az ajánlás vonatkozik a mezőinicializálókra is, ha azok valami terjedelmes objektumot akarnak példányosítani: private otherObject=new OtherBigObject();
Ez azért is különösen veszélyes, mert innentől az OtherBigObject konstruktorára is vonatkozik a "kontruktor probléma" tárgyköre, és ki tudja ki és mit fog az OtherBigObject-be tenni a későbbiekben.
4.3. Modell és jellemzők Ahogy volt már szó a 2. példakód ismertetése során, a modellt magát és a modellben levő propertyket további jellegzetességekkel ruházhatjuk fel. Típusuk és a nevük mellett attribútumokkal jelezhetjük az MVC keretrendszernek, hogy
Hogyan szeretnénk megjeleníteni az adott property tartalmát. Például egy DateTime típusú tulajdonságból csak a dátumot vagy csak az időt. Vagy egy számot ezresekre szeretnénk tagolni. Esetleg nem is szeretnénk, hogy megjelenjen a felületen. Mi legyen az adatmezőhöz tartozó felirat, címke (label) tartalma. Az adatot milyen feltételek szerint tarthatjuk érvényesnek, milyen validációs szabályok érvényesek rá. Ha a modellünk egyben ORM objektum is, akkor az adat tárolását milyen típusú tábla mezőben tároljuk. Egyéb technikai attribútumok
Ezeket az attribútumokat a részben a System.ComponentModel.DataAnnotations névtérben találhatjuk részben az MVC részei. Sajnos a neveik elsőre nem sok tippet adnak, hogy csoportba milyen sorolhatjuk be, ezért szétválogattam ezeket.
4.3.1. Megjelenés HiddenInputAttribute Ezzel dekorált tulajdonság esetében azt tudjuk elérni, hogy annak értéke egy rejtett HTML inputban () 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 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; }
1-52
4.3 Modell - Modell és jellemzők
1-53
Vagy a másik lehetőség az, hogy a Name paraméter egy resource fájl elemét jelenti és akkor egy .resx fájlból fog érkezni a megjelenítendő szöveg. Ekkor definiálni kell a ResourceType-on keresztül azt a resource típust (a .resx fájlból automatikusan generált osztály), amiben a Name által meghatározott publikus tulajdonság szerepel. [Display(Name = "FullNameLabel", ResourceType = typeof(Resources.UILabels))] public string FullName { get; set; }
Fontos, hogy publikus legyen a metódus, mivel ezt majd az MVC keretrendszer fogja felhasználni és nem a mi alkalmazásunk. Ahhoz hogy a resource definíciónk elérhető legyen az MVC számára, a resource generátort értesíteni kell, hogy publikus metódusokat hozzon létre a resource adatok típusos elérését biztosító .Designer.cs háttérfájlban. Ha létrehozunk egy új ’UILables.resx’ fájlt, akkor ezt itt kell beállítani: A resource fájlokkal többnyelvű megjelenítést, így jelen esetben többnyelvű mezőfeliratot is lehet készíteni. Erről egy külön fejezet fog szólni.
DisplayNameAttribute Ez a DisplayAttribute régebbi verziója, amivel csak a címke feliratot tudjuk statikusan megadni. Nincs lehetőség resource fájl elem kapcsolására. Nem érdemes használni csak azért említettem meg, hogy ne keverjük az előzővel, mert nem ugyanaz.
UIHintAttribute Editor és Display sablont határoz meg a DisplayFor és az EditorFor Html helperekhez. Részletesen a View fejezetben tárgyaljuk, mert további részletek ismerete szükséges a használatához.
DataTypeAttribute Ez egy különleges attribútum. Használhatjuk közvetlenül, és akkor megadhatjuk a DataType enum-ban felsorolt típusok közül valamelyiket (DateTime, Date, Time, Duration, PhoneNumber, Currency, Text, Html, MultilineText, EmailAddress, Password, Url, ImageUrl). Vagy leszármazottakon keresztül is lehet használni. Ráadásul egyszerre jelent megjelenítési és validációs szabályt 18 is. Erre egy jó példa a DataType.EmailAddress: [Display(Name = "Vásárló email")] [DataType(DataType.EmailAddress)] public string Email { get; set; }
18
A validációt a böngésző biztosítja és nem az MVC vagy JS kód, ha csak a DataType alap attribútumot használjuk.
4.3 Modell - Modell és jellemzők
1-54
Ha csak megjeleníteni szeretnénk, akkor egy email linket generál belőle az MVC framework (a szövegmező mellett jobbra látható kisbetűs email cím). Ha viszont szerkeszteni szeretnénk, akkor böngésző megköveteli, hogy valódi email címet írjunk be. Ez látható a hibás email címmel a baloldalon. A DataType.MultilineText hatása, hogy többsoros szöveges mező fog megjelenni.
A DataType.Password hatása, hogy jelszó beviteli mezőt kapunk.
A DataType.Date eredménye egy kulturált dátum beviteli mező, amiben háromféle módon is megadhatjuk az értéket. Ráadásul még a várt formátumot is jelzi számunkra. A naptár jellegű kezelésről javascript kód gondoskodik. Még számos további megjelenítést tud eredményezni a használata, amit hamarosan a Html helpereknél fogunk részletesen megnézni.
DisplayFormatAttribute A megjelenő adat formátumát határozza meg a DataFormatString tulajdonságában megadott string formázók alapján. Az előző DisplayType attribútum is meghatároz formázást a különböző típusokhoz, de ezzel az attribútummal azt is felül tudjuk bírálni. Az alapértelmezett viselkedése, hogy a szerkesztőmezőkhöz nem határoz meg formázást, de ezt is ki tudjuk kényszeríteni az ApplyFormatInEditMode = true beállítással. Lentebb az TotalSum propertyre azt határoztam meg, hogy a számértéke pénznem formátumban jelenjen meg. Az LastPurchaseDate dátum+idő típusú pedig csak a dátumot mutatja és fogadja. [Display(Name = "Vásárlások összértéke")] [DisplayFormat(DataFormatString = "{0:C}", ApplyFormatInEditMode = true)] public decimal TotalSum { get; set; } [Display(Name = "Utolsó vásárlás")] [DisplayFormat(DataFormatString = "{0:d}", ApplyFormatInEditMode = true)] public DateTime LastPurchaseDate { get; set; }
Az eredménye egy ilyen oldalrészlet. A bal oldalon szerkesztőmezők, ezek mellett jobbra csak megjelenítés. Mindkettőre kihat.
Ha azonban ezt az oldalt visszaküldjük (submit) az Edit actionnak, azt fogja mondani, hogy nem jó:
4.3 Modell - Modell és jellemzők
1-55
Ez furcsa, nem? Hisz minden jó. Csak annyit kértem, hogy pénznemben szeretném megadni az értéket. Ezért van az, hogy az alapértelmezett működés az, hogy a szerkesztő mezők nem formázottak, mert ilyen és sok hasonló meglepetésben lesz részünk. Az ilyen helyzetekre nincs felkészítve az MVC. Hasonló problémák előkerülhetnek, ha dátumokkal dolgozunk. Azonban miután kitöröljük a szám után a ’Ft’–ot, akkor minden rendben fog lezajlani. A lokalizált adatokkal általában gond szokott lenni, de meg van a lehetőségünk, hogy az MVC viselkedését megváltoztassuk.
4.3.2. Validáció attribútumokkal A validációs attribútumok csak segédeszközök az adatérvényesítés problémájára, leginkább az adat formátumára, értékhatárára, hiányára korlátozódva. Most csak egy felsorolásban végigmegyünk az attribútumokon, és majd a 9.5 Validálás fejezetben fogunk foglalkozni az adat érvényesítés részleteivel. A használatuk annyira egyszerű és automatikus, hogy szinte semmi magyarázatot nem igényelnek. Rárakjuk egy modell propertyre és onnantól az MVC figyelni fogja, hogy a felhasználói felületen bevitt adat megfelel-e az attribútum által lefektetett szabálynak. A ValidationAttribute közös ősből származnak ezek az attribútumok és az alábbi táblázatban soroltam fel. A szövegbezúzás a származási hierarchiát jelenti. Lehetőségek DotNet Framework 4.0 esetében ValidationAttribute CompareAttribute CustomValidationAttribute DataTypeAttribute EnumDataTypeAttribute RangeAttribute RegularExpressionAttribute RequiredAttribute StringLengthAttribute RemoteAttribute
Ha a fentiek nem elégségesessek az igényeink lefedésére, akkor nekiláthatunk, hogy saját validációs attribútumot írjunk. Erről is fog szólni, az bizonyos 9.5 fejezet.
RequiredAttribute Ennek az attribútumnak nincsenek validációs paraméterei. Azzal, hogy ráillesztjük a propertyre az jelzi, hogy kell valami érték. Megjegyzendő dolog, hogy az olyan típusú propertykre hiába rakjuk rá, amelyek nem nullázhatóak. Mivel az Int32 alapértelmezett értéke 0, teljesen elfogadható mint kötelező érték. Hasonlóan a false a booleannál és az 0000.00.00 mint dátum. A string és a nullable típusoknál van értelme a Required attribútumnak. Használatára - hasonlóan a DisplayAttribute-nál látottakhoz - két lehetőség is van. Az egyik amikor a modellbe ’égetem’ a hibaüzenetet, ami egynyelvű alkalmazásnál jöhet csak szóba:
4.3 Modell - Modell és jellemzők [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ó.
Az első paramétere a maximális szöveghosszat jelenti. A MinimumLength opcionális és mint a neve is mutatja a szöveg minimális hosszát jelenti. A fenti esetben a FullName property csak 9 vagy 10 karakter hosszú szöveget fogad el.
MaxLengthAttribute és MinLengthAttribute Az Array és a String típusú propertyvel érdemes használni. A felületen való validációnál az Array-nak sok értelme nincs, így a használata az MVC szempontjából nagyon hasonló a StringLengthAttribute használatához, azzal a különbséggel, hogy ezekkel elég megadni csak az egyik szélsőértéket.
RangeAttribute [Range(100.1, 200.1)] public decimal TotalSum { get; set; }
Alaphelyzetben integer és double értéket lehet megadni az elvárt értéktartomány kikényszerítésére. A példában nyilvánvalóan nem adhatok meg a felületen 100-at az „TotalSum” property értékének. Meghatározhatunk adattípust is, feltéve, hogy az adattípus egyértelműen konvertálható stringből, mivel ilyenkor szöveges formában kell megadni a szélsőértékeket. Ezen kívül az adattípussal kapcsolatban értelmezhetőnek kell lennie a kisebb-nagyobb relációnak. Lehet használni akár DateTime típussal is, aminek a gyakorlati felhasználási lehetősége elég szűkös, mivel az attribútumok paraméterének fordítás időben meghatározottnak kell lennie. A valós helyzetekben a dátum validáció az esetek jó részében az adott naptól számított relatív szélsőértékeket használ (a mai napnál régebbi vagy újabb, egy hónapnál nem régebbi, stb.), dinamikus értéket pedig nem tudunk adni. Majd később látni fogjuk, hogy meg van a lehetőségünk, hogy új dinamikus dátum értékeket validáljuk. [Range(typeof(DateTime), "2010.01.01", "9999.12.31")]
DataTypeAttribute Ezzel már találkoztunk a megjelenítésre szakosodott attribútumok csoportjában, és említettem hogy ez validációs funkciójú is. De csak akkor, ha a származtatott attribútum verzióját használjuk! Szó volt arról, hogy a megjelenítését felül lehet bírálni a DisplayFormatAttribute-al. Ennek nem kívánt
1-56
4.3 Modell - Modell és jellemzők mellékhatásai lehetnek, ha meggondolatlanul, egymásnak ellentmondóan állítom be a két attribútumot. Például az adat típus csak idő, a megjelenítési formátum csak dátum: [Display(Name = "Utolsó vásárlás")] [DataType(DataType.Time)] [DisplayFormat(DataFormatString = "{0:d}", ApplyFormatInEditMode = true)] public DateTime LastPurchaseDate { get; set; }
Validációs hibát nem okoz, de a megadott adat, mint idő nem fog visszajönni a mentés után az ellentmondásos attribútumok miatt. Még egyszer kiemelném: Egy DataType(DataType.EmailAddress) nem ír elő email validációs szabályt az MVC számára, kizárólag a leszármazottak definiálják. Jogos kérdés, hogy akkor meg hogy lehet az, hogy a jobb oldali kép mégis azt mutatja, hogy hibás email cím esetén validációs üzenetet kapunk? Ha megnézzük a generált HTML kódot, ezt a sort találjuk az email mezővel kapcsolatban:
A vastagon kiemelt definíció szerint a validációt a böngésző biztosítja, de csak HTML 5 esetén, mivel a fenti type="email" definíció csak innentől érhető el. Ezek a DataType attribútum definíciók generálnak új HTML 5 új beviteli mezőtípusokat, amiknek a validálása a böngészőre van bízva, ha mi más validációt nem írunk elő: [DataType(DataType.Url)] [DataType(DataType.EmailAddress)] [DataType(DataType.PhoneNumber)] [DataType(DataType.DateTime)] [DataType(DataType.Date)] [DataType(DataType.Time)] [DataType(DataType.Url)]
A valódi, MVC által kezelt validációs megoldásokat a DataType attribútum leszármazottjai biztosítják. A .Net 4.0 alatt csak az EnumDataTypeAttribute érhető el, de .Net 4.5 alatt van több ilyen leszármazott is. Az attribútumok funkciói a nevük alapján szerintem kitalálhatók, és az is hogy milyen validálási szabályt írnak elő. EnumDataTypeAttribute, CreditCardAttribute, EmailAddressAttribute, FileExtensionsAttribute, PhoneAttribute, UrlAttribute
A FileExtensions attribútumról annyit azért érdemes tudni, hogy a böngészőben, a feltöltésre szánt fájlok kiterjesztését lehet vele meghatározni. A CreditCardAttribute, EmailAddressAttribute, FileExtensionsAttribute, UrlAttribute attribútumok az MVC Futures nevű projektből kerültek át a .Net 4.5be. 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.
1-57
4.3 Modell - Modell és jellemzők [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
1-58
4.3 Modell - Modell és jellemzők System.ComponentModel.DataAnnotations névtérben érhető el. A .Net 4.0 alatt viszont a System.Web.Mvc névtérben lehet megtalálni. Emiatt egy 4.0 -> 4.5 verzióváltás esetén némi plusz munkát jelent a névterek pontosítása. Az MVC 5-ben a System.Web.Mvc névtér alatti verziót elavultnak jelölték (obsolete), tehát kerüljük a használatát. Az attribútum elnevezése szerintem zavaró. Jobb lett volna egy EqualsAttribute név, végül is csak egyenlőség vizsgálatára jó. A Compare név számomra azt sugallja, hogy egy paraméterrel megadhatok egy relációs műveletet (<, >, <=, >=, !=) a propertyk értékeinek összehasonlításához, de erre nincs lehetőség. Ezzel csak az akartam hangsúlyozni, hogy az MVC-be és a DataAnnotations-be sincs beépítve, igazi komparátor attribútum.
RemoteAttribute Ennek segítségével úgy oldhatjuk meg a böngésző oldali validációt, hogy a validálandó inputmező tartalma egy kontroller actionben értékelődik ki. Normál esetben a kliens oldali validációt a böngészőben futó JS kódban kell implementálni. Itt most nem készítettem példát, mert annál összetettebb dologról van szó, viszont később megnézzük részletesen egy külön alfejezetben.
RegularExpressionAttribute Amit meglehet fogalmazni regular expression-nel, azt fel tudjuk használni validációra is. A példában a FullName mezőbe csak betűket és szóközt tartalmazó szöveget lehet megadni, amelynek hossza 1 és 20 között van. [RegularExpression(@"^[a-zA-Z'\s]{1,20}$")] public string FullName { get; set; }
Ennek az ErrorMessage paraméterét mindenképpen töltsük ki, még akkor is ha angol nyelvű, végtelen türelmű, vak ügyfél számára fejlesztünk, ugyanis az alapértelmezett üzenettől biztos hanyatt vágja magát: The field Felhasználó név must match the regular expression '^[a-zA-Z'\s]{1,20}$' Nem mellékesen a .Net 4.0 alól hiányzó email validációs attribútum, egy jól megfogalmazott reguláris kifejezéssel pótolható. Amúgy a .Net 4.5 alatt ezt szintén regex-el oldják meg. A reguláris kifejezések validációs helyzetek lefedésére számtalan példa kering a neten. Azonban legyünk nagyon figyelmesek, mivel a relatív kislélekszámban beszélt magyar nyelvre (és más nyelvekre is, amik nem csak az angol abc szűk készletére korlátozzák a karakterkészletüket) nincsenek tekintettel az angolszász, csípőből lökött példák. Például azt a tényt, hogy egy domain név már tartalmazhat ékezetes karaktereket (ami egy URL-ben vagy email címben is megjelenhet) sok példa nem veszi figyelembe.
AllowHtmlAttribute Alapértelmezetten az MVC nem engedi, hogy a propertybe érkező szövegben HTML markup legyen. Ezzel az attribútummal be tudjuk engedni a HTML tartalmat. Hatására a háttérben le lesz tiltva az un.
1-59
4.3 Modell - Modell és jellemzők request validation, a beérkező kérés feldolgozása alatt az adott propertyre. Majd az action filtereknél látni fogjuk, hogy ezzel még nem teljes az engedély, mert ezt az action szinten is meg kell engedni a ValidateInputAttribute(false) segítségével .
CustomValidationAttribute Ez az attribútum, a kódból egyedileg megfogalmazott validációt segíti. A validálandó property legyen megint a FullName, ami nem tartalmazhat számot. Tegyük fel, hogy most viszont nem szeretnénk erre egy regex kifejezést írni, hanem favágó módszerrel esünk neki. [CustomValidation(typeof(ValidationDemoModel), "ValidateFullName")] public string FullName { get; set; }
Az attribútum első paramétere egy osztálytípus. A második az osztály egy publikus statikus metódusának neve szövegesen, amely metódus fogja végezni a validációt és egy darab (!) 19 ValidationResult objektummal kell visszatérnie. A validációt végző osztálytípus most az egyszerűség kedvéért a modell maga, de lehetne egy külső osztály is. public static ValidationResult ValidateFullName(string fullName) { if (string.IsNullOrWhiteSpace(fullName)) return new ValidationResult("A nevet meg kell adni!"); if (fullName.IndexOfAny("0123456789".ToCharArray()) >= 0) return new ValidationResult("A név nem tartalmazhat számot!"); return ValidationResult.Success; }
A ValidateFullName a ValidationResult-ba csomagolt hibaüzenettel tér vissza, ha a "fullName" üres vagy számot tartalmaz. Megfelelő "fullName" esetén, a Success statikus tulajdonságban elérhető „hibátlan validáció” értelmű objektummal válaszol. Ezzel a módszerrel teljesen egyedi, property szintű validációt tudunk definiálni. Szemben az eddigi validátor attribútumokkal, ez használható osztályszintű validátorként is. Megoldhatunk ezzel olyan adatellenőrzést, amikor több tulajdonság tartalmának kell konzisztensnek lennie, együttesen értelmezhetőnek, érvényesnek vagy érvénytelennek. Ilyen szituáció lehet, ha nem kötelező megadni a címet és az email címet egyszerre, elég ha az egyik kitöltésre kerül. Ezt property szinten csak körülményesen, a CustomValidation-al viszont könnyen tudjuk érvényesíteni. Díszítsük ki vele az osztályt és implementáljuk a hozzá tartozó metódust. [CustomValidation(typeof(ValidationDemoModel), "ValidateDemoModel")] public class ValidationDemoModel { // A normál modellpropertyk public static ValidationResult ValidateDemoModel(ValidationDemoModel tovalidate) { if (string.IsNullOrEmpty(tovalidate.Address) && string.IsNullOrEmpty(tovalidate.Email)) return new ValidationResult("A címet vagy az email címet meg kell adni!"); return ValidationResult.Success; } }
Az előbb nem említettem, de a validátor metódus paraméter típusának meg kell egyeznie azzal a típussal, amire a CustomValidation attribútumot tettük. Ezért itt most a paraméter típusa nem string,
19
Ez egy korlát, amit majd a validációkkal foglalkozó fejezetben ledöntünk.
1-60
4.3 Modell - Modell és jellemzők hanem maga a validálandó osztály. A logika implementálása egyetlen sor, ezért nem lenne érdemes egy külön ValidationAttribute leszármazottat írni (bár azt is megfogjuk majd próbálni). A hibás validáció eredménye is eltér az eddigiektől, mivel nem köthető propertyhez, ezért a beviteli mezők felett jelenik meg. És ha több validáció is elhasalna, akkor azok is itt lennének felsorolva, piros gombóccal felvezetve. A felhasználónévben továbbra sem szabad számot megadni. A ValidationResult-ban megvan a lehetőség, hogy egy vagy akár több inputmezőhöz rendeljük a megadott üzenetet. Ehhez még a MemberNames nevű és IEnumerable<string> típusú paraméterét kell kitölteni. Az alábbi példában egy string elemtípusú tömböt adtam át, paraméterként: return new ValidationResult("A címet vagy az email címet meg kell adni!", new[] { "Address", "Email" });
Sajnos, azonban ez ebben a helyzetben a CustomValidation-nel nem működik. Máshol az MVC-ben (és más platformon is) igen, amit majd a validációkat részletező fejezetben meg is nézünk. Ide tartozik, hogy az osztályszintű validáció kisebb prioritású, mint a tulajdonság szintű. Ezért addig, míg az egyedileg szabályozott propertyk közül egy vagy több értéket hibásan adunk meg és ezek mellett mind megjelennek a hibaüzenetek, addig az osztályszintű custom validáció ki sem fog értékelődni, míg a property szintű, validációk által felügyelt mezőket nem javítjuk ki.
A sorrend: Az egyes propertykre több validációs attribútumot is rakhatunk. Ilyenkor az első hibás validáció esetén a továbbiak nem fognak kiértékelődni és ilyen helyzetben - nem úgy, mint a property-osztály viszonylatban - a CustomValidation-nak elsőbbsége van. Másként fogalmazva, a propertyn levő CustomValidation fog kiértékelődni elsőként, az modellosztályon definiálva viszont a propertyken levő validációs attribútum(ok) után.
1-61
4.3 Modell - Modell és jellemzők A validációs attribútum példák egyben Idemásoltam, hogy látható legyen, eddig milyen attribútumok kerültek szóba. Némelyik ki van kommentezve, mert értelmetlen lenne ha azokat is engednénk érvényesülni. Pl. Nem lehet két Required attribútum egy propertyn. [CustomValidation(typeof(ValidationDemoModel), "ValidateDemoModel")] public class ValidationDemoModel { [HiddenInput(DisplayValue = false)] public int Id { get; set; } [Display(Name = "FullNameLabel", ResourceType = typeof(Resources.UILabels))] [Required(ErrorMessage = "A név megadása kötelező (1)!")] //[Required(ErrorMessageResourceName = "UserNameRule", // ErrorMessageResourceType = typeof(Resources.Validations))] //[StringLength(10, MinimumLength = 9)] //[DataType(DataType.Password)] //[DataType(DataType.Url)] [CustomValidation(typeof(ValidationDemoModel), "ValidateFullName")] //[RegularExpression(@"^[a-zA-Z'\s]{1,20}$", // ErrorMessage = "Kötelezően csak az angol ABC betűi lehetnek, maximálisan 20 karakter hosszúságban!")] public string FullName { get; set; } [Display(Name = "Vásárló címe")] [DataType(DataType.MultilineText)] //[Required(ErrorMessageResourceName = "CimRule", // ErrorMessageResourceType = typeof(Resources.Validations))] public string Address { get; set; } [Display(Name = "Vásárló email")] [DataType(DataType.EmailAddress)] //Dotnet 4.5: [EmailAddressAttribute] public string Email { get; set; } [Display(Name = "Vásárlások összértéke")] [DisplayFormat(DataFormatString = "{0:g}", ApplyFormatInEditMode = true)] //[Range(100.1, 200.1)] public decimal TotalSum { get; set; } [Display(Name = "Utolsó vásárlás")] [DataType(DataType.Date)] [Range(typeof(DateTime),"2010.01.01","9999.12.31")] //[DataType(DataType.Time)] [DisplayFormat(DataFormatString = "{0:d}", ApplyFormatInEditMode = true)] public DateTime LastPurchaseDate { get; set; } [Display(Name = "Vásárló típus")] [EnumDataType(typeof(CustomerTypeEnum))] public CustomerTypeEnum CustomerType { get; set; } public static ValidationDemoModel GetModell(int id) { if (datalist == null) datalist = new Dictionary(); if (!datalist.ContainsKey(id)) { datalist.Add(id, new ValidationDemoModel() { Id = id, FullName = "Tanuló " + id, Address = string.Format("Budapest {0}. kerület", id + 1), Email = "[email protected]", TotalSum = id * 2345.45m, LastPurchaseDate = DateTime.Now.AddDays(-2 * id) }); } return datalist[id]; } private static Dictionary datalist; public static ValidationResult ValidateFullName(string fullName) {
1-62
4.4 Modell - Modell és tárolás. Adatperzisztencia if (string.IsNullOrWhiteSpace(fullName)) return new ValidationResult("A nevet meg kell adni!"); if (fullName.IndexOfAny("0123456789".ToCharArray()) >= 0) return new ValidationResult("A név nem tartalmazhat számot!"); return ValidationResult.Success; } public static ValidationResult ValidateDemoModel(ValidationDemoModel tovalidate) { if (string.IsNullOrEmpty(tovalidate.Address) && string.IsNullOrEmpty(tovalidate.Email)) return new ValidationResult("A címet vagy az email címet meg kell adni!", new[] { "Address" }); return ValidationResult.Success; } public enum CustomerTypeEnum { [Description("Nem ismert")] Unknown, [Description("Magán személy")] Person, [Description("Kiskereskedő")] Retailer, [Description("Nagykereskedő")] Supplier } }
4.4. Modell és tárolás. Adatperzisztencia Hacsak nem valami egészen speciális MVC alkalmazást készítünk, szükségünk lesz arra, hogy a modellünk tartalmát eltároljuk hosszabb időre. Szintén általános jellemző, hogy a modell tartalmát, meglévő adatbázis adatokból állítjuk össze. A típusos modellünkben levő adatok (adatbázisban) tárolását szokták perzisztálásnak nevezni. Ahhoz, hogy a modelladatok perzisztensek legyenek, szükség van egy adatkezelési rétegre, ami elvégzi az adatforrásból érkező adatok és a modellosztály propertyjeiben tárolt adatok közti transzformációt. Más szóval az adatbázis mezőket megfelelteti a modell propertyjeivel. A megfeleltetés során gyakran típuskonverziót is kell végezni. Ezeket a műveleteket elvégző eszközöket Object-RelationalMapper-ek végzik el. (ORM20). Mostanra több ilyen ORM létezik a .Net környezetben: NHibernate, Entity Framework (Microsoft), OpenAccess ORM (Telerik), XPO (Devexpress), LLBLgenPro, hogy csak azokat soroljam fel, amikkel már volt kisebb-nagyobb tapasztalatom. Némelyik ingyenes, némelyikért fizetni kell. A legtöbbjük többféle adatbáziskiszolgálóval képes működni. Az hogy Oracle, MSSQL, SQLite, Access vagy más adatbáziskezelőhöz kapcsolódnak az nekik közel mindegy. Legalább két fontos előnye van, ha ilyen ORM-et használunk. Egyrészt pár kattintással előállítható a modell az adatbázisból vagy egy XML-ben meghatározott sémából, vagy fordítva, az adatbázis állítható elő a modell kódjából (ahogy láttuk a 3.8 fejezetben), vagy XML-ből. A másik előny, hogy a validációs adatok és display nevek, szintén előállíthatóak az adatbázis séma alapján. Ez utóbbi nagyon kényelmes, de néha hátrányos is lehet, ha például az automatikusan generált property attribútumok nem úgy állnak össze, ahogy a mi speciális igényünknek megfelel. Ilyenkor jön a modellreszelés (finomhangolás) a partial osztályok és más rafinált lehetőségek.
4.4.1. Adatbázis séma szerinti modellek Ebben az esetben a modellosztályok közel pontos megfelelői az adatbázis táblák sémájának. A tábla minden egyes mezője egyértelműen 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. 11. ábra
Ami erre a modell megvalósítási típusnál célravezető, hogy ne legyen szoros kapcsolatban az adatkezelési réteg egyik összetevőjével sem. Teljesen le legyen választva, és ne legyen hivatkozása az adatkontextusra (pl. UnitOfWork, DataSet, DbContext, Session). Az adatkontextus alatt olyan infrastruktúrát szoktak érteni, ami a külső adatszolgáltatóhoz kapcsolódva (pl. adatbázis szerver) összefüggő adatokat szolgáltat az alkalmazás felsőbb rétegei számára és adatokat fogad a felsőbb rétegtől, amiket az adatszolgáltatónak továbbít. Ezeket egységben kezeli, pl. táblákat és köztük levő relációkat vagy objektumokat és ezek közti referenciákat. Felületet biztosít a szokásos adatműveletekre (CRUD), mint pl. create, retrieve, update, delete, tranzakció kezelés, lapozás. A hátrány az MVC modell szempontjából, hogy az ORM-ek közül némelyik állapotfüggő segédinformációkat helyez el az általa felügyelt osztályokban, így az modellünkben is. Így ezekkel a session/context adatokkal magukhoz láncolják azokat. Nagyon jellemző, hogy az ilyen adatkontextusok IDisposabe interfész alapúak, és elvárják hogy tényleg le is kezeljük annak Dispose igényét. Sajnos az MVC-nek még akkor is szüksége van a modellre, amikor már átkerült a végrehajtás az MVC infrastruktúrájába, és a Dispose metódust nem tudjuk meghívni. Ezt is érdemes szem előtt tartani az ORM kiválasztásánál és használatánál. Ahogy már láttuk az Entity Framework nagyon jól használható az MVC-vel, mert az adatmodell osztályai nem függenek az EF adatkontextusától (DbContext) Egy másik ok, hogy ne legyen szoros kapcsolat az adatkezelési réteggel az, hogy az MVC infrastruktúrája is tudja példányosítani számunkra a modellt (model binder). Ez nagyon kényelmes tud lenni, de ha az ORM nem alkalmas rá, akkor fájdalmassá válik a használata. A legalkalmasabb ebben az esetben egy olyan modellosztály, ami nem öröklődik más osztályból, nincsenek függőségei, csak minimális kód van benne, és a propertyjei pontosan megfeleltethetőek az adatbázis mezőinek. Konkrétan a nevük is egyezik és a típusuk is (már amennyire lehetséges). Szokták az ilyen mindenre használható, független, nyers modellobjektumot POCO-nak nevezni, de nem azért mert olyan kicsi, mint egy pocok, hanem az angol rövidítés szerint: Plain-Old-CLR-Object. Egyes 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
4.4 Modell - Modell és tárolás. Adatperzisztencia
1-65
kell beállítani, esetleg klónozni, hogy az megfeleljen az ORM igényeinek. Ez pedig jelentős többletmunkával jár. További jellemzője ennek a modellmegvalósításnak, hogy mivel ennyire adatbázis vagy tárolásközeliek a modellosztályok, így a tárolással kapcsolatos jellemzőket is magával hordozhatják. Például:
automatikus entitás id generáltatás, tranzakció- és konkurenciakezelés csak relációban elérhető adatok esetleg szükségtelen teljes objektum gráfok. (minden tábla-osztály hozzáférhető). (Nem lazy loading)
A tárolásközeli technikai adatok kezelésében és az előbbiekben említett állapotfüggő adatok leválasztásában segítség lehet, ha beiktatunk még egy un. Repository (repository pattern21) réteget az ORM felügyelt osztályok és a modell felhasználás közé, ami leválasztja az ORM-ről a modellosztályainkat. Kisebb rendszerekben gondolkozva, ez a réteg elsőre feleslegesnek fog látszani és többletmunkát is jelent, de érdemes ott tartani a tarsolyunkban a tudást, hogy van ilyen is. Az előbb felsorolt három jellemzőből a két utolsó szokott problémát okozni és jó megoldást csak ügyes tervezéssel lehet biztosítani. Két ilyen megközelítést említenék: Az egyik, hogy az objektumgráfot szétszedjük valóban szoros kapcsolatban levő csoportokra, és ezt használjuk az alkalmazásunkban. A csoportok közti kapcsolatot pedig, manuálisan feltöltött modellpéldányokkal tartjuk fenn. Ilyenkor egy modell csak egy csoportban szerepel. Ezt azért elég nehéz megoldani és az adatbázis séma tervezésekor is figyelembe kell venni. A másik, hogy a csoportokat úgy alakítjuk ki, hogy egy-egy adathalmazigény számára csak a minimálisan szükséges objektumokkal foglalkozzon. Ilyenkor egy-egy objektum több kontextusban is tud szerepelni. Ez már nem függ annyira az adatbázis séma megvalósításától. Ezt a metodikát multiple data contextnek vagy bounded data contextnek szokták nevezni. Ezt szemlélteti az ábra. Példaként: ha az oldalunk a beszerzések kezelésével foglalkozik, a „Beszerzés” kontextust használjuk mivel biztosan nincs szükségünk az Alkalmazottak (területi képviselők) HR adataira.
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; }
4.5 Modell - Az érinthetetlen, generált modell problémája 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.
4.5. Az érinthetetlen, generált modell problémája Abban az esetben, ha a modelljeink mind, vagy legalábbis a jelentős része meglévő (pl. adatbázis) séma szerint épül fel, akkor kicsit gondban leszünk a property szintű attribútumok használatával. Ilyen helyzetet tudnak okozni az ORM-ek és a szolgáltatások, amikor is a számukra generált osztályokkal kell dolgozni. Ilyenkor a modellosztályunkat az adatbázis vagy valamilyen XML, WSDL fájlban meghatározott séma szerint generáltatjuk a Visual Studio-val, vagy más eszközzel. Ezt a generálást időről-időre, ahogy változik az adatbázis ORM struktúra vagy a service definíciója, újra le kell futtatni. Aminek az lenne az eredménye, hogy a generált fájlba a propertykre manuálisan helyezett attribútumok elvesznek, ha ilyet megpróbálnánk. Megpróbálhatjuk belerakni a kódgenerátorba az attribútumokat, de általában az fix sémadefiníció nem tartalmazza annyira részletesen jól kifejtve a validációs és egyéb szabályokat legfeljebb azokat, amik a perzisztencia vagy a kapcsolat vezérlésére használatosak. Általában csak olyan jellemzőket, mint a mezőben tárolható karakterek mennyiségét, DB adattípust, és egyéb technikai jellemzőket (melyik a primary key, melyik a timestamp). Így a generált modell nem fogja tartalmazni a helyes validációt. Hogyan tudjuk mégis kiegészíteni a modellünket és a propertyket attribútumokkal úgy, hogy ne függjünk az automatikusan generált osztálytól?
MetadataTypeAttribute Ezt a problémát egy un. ’buddy class’-al tudjuk frappánsan megoldani. Szükségünk van egy MetadataType attribútummal dekorált partial class-ra, az automatikusan generált (ORM) osztály mellé. Az ORM osztálygenerátorok általános jellemzői, hogy nyitva hagyják a bővíthetőséget és
1-67
4.6 Modell - Egyéb modellattribútumok
1-68
részleges osztályokat (partial class) hoznak létre a generált típusok definíciójában. A MetadataType egy típust vár, ez a buddy class, ami azonos nevű és típusú propertyket tartalmaz mint a generált osztály. Ezekre a propertykre ráaggatott validációs attribútumokat úgy fogja értelmezni az MVC framework, mintha az eredeti generált ORM osztály azonos nevű tulajdonságain lennének. A Person osztályt automatikusan generáltatjuk, tehát "érinthetetlen". Most az áttekinthetőség kedvéért, csak egy tulajdonsággal: public partial class Person { public string FirstName {get;set;} }
A fenti osztály partial párja látható a 6. példakódban a felső részen, amiben az osztály megkapta a MetadataType attribútum paraméterében a PersonMetadata osztály típusát. Másra itt nincs szükség. A PersonMetadata osztályban pedig az azonos nevű és típusú FirstName propertyre akasztottam rá a Required és a Display attribútumokat. [MetadataType(typeof(PersonMetadata))] public partial class Person { } public class PersonMetadata { [Required] [Display(Name = "First Name")] public string FirstName {get;set;} }
6. példakód
Ezt a metodikát érdemes alaposan megérteni, mert később még használni fogjuk. Persze nincs szükség erre a trükkre, ha a modellosztályokat mi határozzuk meg és az osztályok alapján készítjük vagy készítetjük el az adatbázis sémát. Úgy tűnik, hogy ez utóbbi az un. ’code-first’ megközelítés, jóval használhatóbb az MVC-vel kapcsolatban, ha ORM-ről van szó. Nem is értem miért kellett ezzel ennyit várni az Entity Framework esetében.
4.6. Egyéb modellattribútumok ScaffoldColumn Előfordulhat, netán ideiglenesen, hogy egy model propertyt mégsem szeretnénk megjeleníteni. Ha ezzel az attribútummal láttuk el a propertyt, egyszerűen nem fognak létrejönni a HTML markupok. Akkor sem, ha a View-ban ott vannak a Html helperek. Ha a modellhez nem készítünk View-t vagy más sablont, az MVC lehetőséget ad az EditorForModel vagy a DisplayForModel Html helperekkel, hogy dinamikusan generált View sablont használjunk. Mivel ilyenkor a Html helpereket nem mi írjuk a 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,
4.7 Modell - Egy demó modell amúgy nem érdemes a kettőt egy propertyn használni. Ezekről később a 8.1 fejezetben lesz részletesen szó.
4.7. Egy demó modell Ahhoz, hogy a további részekben ki tudjuk próbálni a lehetőségeket, szükségünk lesz egy fapados modellre. using using using using using
namespace MvcApplication1.Models { public class ActionDemoModel { [HiddenInput(DisplayValue = true)] public int Id { get; set; } [Display(Name = "FullNameLabel", ResourceType = typeof(Resources.UILabels))] public string FullName { get; set; } //[AllowHtml] [Display(Name = "Vásárló címe")] //[DataType(DataType.MultilineText)] public string Address { get; set; } [Display(Name = "Vásárló email")] [DataType(DataType.EmailAddress)] //Dotnet 4.5: [EmailAddressAttribute] public string Email { get; set; } [Display(Name = "Vásárlások összértéke")] public decimal TotalSum { get; set; } [Display(Name = "Utolsó vásárlás")] [DisplayFormat(DataFormatString = "{0:d}", ApplyFormatInEditMode = true)] public DateTime LastPurchaseDate { get; set; } [Display(Name = "Vásárlások listája")] public IList 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(); if (!datalist.ContainsKey(id)) { var products=ActionDemoProductModel.CreateProducts(); datalist.Add(id, new ActionDemoModel { Id = id, FullName = "Tanuló " + id, Address = string.Format("Budapest {0}. kerület", id + 1), Email = "[email protected]", TotalSum = id * 345.45m, LastPurchaseDate = DateTime.Now.AddDays(-2 * id), PurchasesList = products, KeyPurchase = products[2] });
1-69
4.7 Modell - Egy demó modell
1-70
} return datalist[id]; } public static IList GetList() { return datalist.Select(dl => dl.Value).ToList(); } public SelectList GetSelectList() { return new SelectList(this.PurchasesList, "Id", "ProductName",this.KeyPurchase.Id); } private static Dictionary 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 CreateProducts() { var rand = new Random(); int count = rand.Next(5, 10); var result = new List(count); for (int i = 0; i < count; i++) { result.Add(new ActionDemoProductModel { Id = ++tid, ItemNo = string.Format("szam{0}-k{1}", i, DateTime.Today.Day), Quantity = rand.Next(1, 1000), ProductName = string.Format("{0}{1}", ProductNames[rand.Next(ProductNames.Length)], tid * 1001) }); } return result; } private static readonly string[] ProductNames = new[] { "Szék", "Ágy", "Asztal", "Párna", "Tükör", "Polc" }; #endregion } }
7. példakód
Ez a modell egy minimalista tárolóosztály. A GetModell statikus metódus a datalist szótárból visszaadja az id paraméternek megfelelő modell példányt. Ha nincs ilyen, akkor példányosít egyet, felparaméterezi, és azt adja vissza. Ezzel meg is valósítottunk egy butus tárolót.
5.1 A kontroller és környezete - Az alkalmazásunk beállítása. A web.config
5. A kontroller és környezete
Az MVC kódkörnyezetéről szóló téma bevezetéseként szeretnék rávilágítani, hogy a kontroller és a benne levő kódok laza kapcsolatban vannak a kódot indító eseményekkel. Ezt a kapcsolatot külön kell deklarálni és ez elég dinamikus tud lenni. A lefutó action metódus kiválasztása egy sor deklaratívan meghatározott szabályok együttes eredményeként történik meg. Ellenpéldaként említeném az ASP.NET Web Forms alkalmazás alapértelmezett működését. Ott a code-behindban implementált kód indítása, a kódhoz tartozó aspx oldal lekéréséből fakad. A másik ellenpélda a Winforms alkalmazás, ahol egy ablak megnyitását gombok és menüelemek eseménykezelőiben futó kód végzi el. Az, hogy az MVC alkotói ilyen lazán csatolt megoldást választottak, lehetőséget és irányelvet teremtettek arra, hogy az implementált metódusok és osztályok ne függjenek erősen a keretrendszertől, mint futtató környezettől. Ezzel nyitva hagyták a lehetőséget, hogy a kódok unit tesztelhetőek legyenek, és hogy a keretrendszert kiegészítsük, vagy funkcióit lecseréljük anélkül, hogy ezzel a rendszer más részeinek a működését megzavarnánk. Bárcsak minden keretrendszer ennyire flexibilis lenne!
5.1. Az alkalmazásunk beállítása. A web.config A legtöbb webszerver (IIS, Apache, stb.) működését szöveges konfigurációs fájlokkal tudjuk befolyásolni. Ezekben megadhatjuk, hogy milyen jogosultságellenőrzést szeretnénk, milyen további bővítő modulokat kívánunk használni, hogyan tudjuk elérni az adatbázis szervert, és további paraméterekkel részletesen beszabályozhatjuk a webalkalmazás működését. Nézzük meg a projekt gyökerében levő web.config-ot. Ez egy jól definiált XML fájl. A tartalmát nem másolom ide, inkább felhívnám a figyelmet arra, hogy a beállítások szekciókra vannak bontva, attól függően, hogy a webkiszolgálás mely szereplőjére vonatkozik. Igen, a teljes webkiszolgálást testre szabhatjuk, a webszervert és a mi alkalmazásunkat is. Egy szélsőséges példa, ami a ág alatt szokott lenni, az un. assembly redirekció, amivel előírhatjuk a .Net keretrendszer számára (!), hogy az esetlegesen igényelt régi verziójú (pl. System.Web.Mvc 2.0.0.0) assembly-k helyett az új 4.0.0.0 verziót legyen kedves használni. Ez a beavatkozási mélység pedig azt mutatja, hogy a web.config az alkalmazásunk Achilles-sarka. Nagyon fontos, hogyha beleírunk valamit ebbe, akkor azt meggondoltan tegyük, főleg ha csapatban dolgozunk vagy az élesben használt alkalmazásról van szó. Ezeknek a web.config fájloknak van még egy jó tulajdonságuk. Az, hogy a benne foglaltak az adott mappára és annak almappáira is vonatkoznak (némely kivétellel). Minden almappában elhelyezhetünk további web.config-ot, amivel kiegészíthetjük vagy felülbírálhatjuk a szülő mappában levő web.config fájlok beállításait. Erre az MVC-ben is van példa. A Views almappában van egy másik web.config. Ebben javarészben az van megfogalmazva, hogy a projekt struktúra Views mappájából nem lehet kiszolgálni semmit, a tartalma nem hozzáférhető az URL alapján. Próbáljuk meg mit kapunk, ha megpróbáljuk megcélozni a /Views/Home/About.cshtml fájlt, vagy a /Views/Home/ vagy /Views/ mappát. Ha készítünk egy index.html fájlt, majd megpróbálhatjuk megnyitni a böngészőből, akkor egy „The resource cannot be found” üzenet fog érkezni, mivel ennek a mappának a web.config-ja előírja, hogy bármilyen kérést a
1-71
5.1 A kontroller és környezete - Az alkalmazásunk beállítása. A web.config 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 és a szakaszokat, pl. így: <system.web> . . . <system.webServer> 8. példakód
A ’. . .’ olyan szakaszt jelöl, ami most nem lényeges, de azért ne töröljük ki onnan, ami gyárilag ott van. Alkalmazás újraindítás után URL-nek adjuk meg a /View/Home/Index.html és a tartalma meg fog jelenni. Most nézzük meg a nyitólapot és lám minden jól működik. Miközben még sincs minden rendben, ugyanis egy nem nyilvánvaló rést ejtettünk az alkalmazás biztonsági beállításán. Tehát fontos, hogy a web.config-ban vannak olyan szakaszok, amelyeket tényleg nem érdemes piszkálni addig, amíg pontosan nem tudjuk meg miért is vannak ott. Látszólag nem is okozott problémát, minden működik. Vajon mikor derülne ki, hogy biztonságilag hibás a web.config, ha így hagynánk? Ha megnéztük mindkét web.config fájlt, azt gondolhatjuk, hogy nem kell szinte semmit sem beállítani, hisz alig van benne valami. Valójában az alkalmazásunk gyökerében levő web.config azért ilyen "üres", mert ez a web.config nem az abszolút értelembe vett root konfiguráció. Ez a konfigurációs fájl valójában csak a sokadik eleme egy leszármazási láncnak. A lánc elején áll a Machine.config, ami tartalmazza az összes adott .Net keretrendszer verziót használó alkalmazás és szerver alapbeállításait. Ez .Net 4 64 bites verzió esetén nálam ezen ez útvonalon volt elérhető: Windows\Microsoft.NET\Framework64\v4.0.30319\Config\. A következő konfigurátor fájl az applicationHost.config. Ezt attól függően, hogy normál IIS-t vagy IIS Expresst használunk, más helyen kell keresni. A fejlesztéssel kapcsolatban most számunkra az Express a fontosabb. Ennek az elérési útja a felhasználói profilban van: c:\Users\[a Windows felhasználó név]\Documents\IISExpress\config\ Érdemes belenézni, mert nagyon sok beállítás itt van meghatározva, amire szükségünk lehet. Talán a legfontosabb része a Sites felsorolás. Itt vannak azok a webalkalmazások, amiket a Visual Studio-ból indítottunk el IIS Expresst használva. ("Use Local IIS Web server" beállítás a projekt beállítások ablakban a Web fül alatt). <sites> <site name="WebSite1" id="1" serverAutoStart="true">
1-72
5.2 A kontroller és környezete - Az alkalmazás kiindulási pontja. A global.asax <site name="FirstMVCApp" id="2"> <site name="MvcApplication1" id="3">
Az kiemelt 'MvcApplication' a demóalkalmazás neve, ami a könyvben szereplő példákat tartalmazza. A fejlesztés során a leghasznosabb sorát, a bindings listát szintén kiemeltem. Az első 'binding' a normál beállítás. A következő sorral el lehet érni, hogy az IIS Express a gépünkön kívülről jövő kéréseket is kiszolgálja a megadott IP címen ( ami a gépem IP címe volt éppen). Az adott porthoz a tűzfalat is ki kell nyitni. A következő a konfigurációs láncolatban a machine.config–gal azonos mappában levő web.config fájl. Ebben már nagyon sok beállítást találunk. Például ami részletesen meghatározza, hogy a különböző kiterjesztésű fájlok kiszolgálása/feldolgozása melyik szerver modul feladata legyen. Előfordul, hogy valahol a neten egy cikket olvasunk, amiben leírják, hogy ezt és ezt kell beállítani a web.config-ban. Sokszor kimarad, hogy mégis hova, melyik szekcióba kéne tenni azt a néhány emlegetett beállítást. Ezért is jó tudni ezekről az "ős" beállító fájlokról. Ha az alkalmazásunk web.config fájlját szeretnénk bővíteni valamilyen beállítással, akkor a machine.config melletti web.config-ot elővéve nagy esélyünk van rá, hogy azonosítani tudjuk a keresett szakaszt. A másik nagyon jó érv, hogy megjegyezzük ezeket az, hogy a normál web.config és machine.config fájlokból van egy-egy .comments kiterjesztésű fájlváltozat is. Ezek belsejében fel vannak sorolva a beállítások mellett a lehetséges további paraméterek és értékkészletük, típusuk sok-sok komment sorban. Az önleíró változatok. Ezek mellett még vannak előkészített beállítássablonok is különböző biztonsági szintekhez. Hmm, hol is láttam ilyeneket régen? Ja, igen: httpd.conf, my.ini, php.ini.
5.2. Az alkalmazás kiindulási pontja. A global.asax Mi történik az után, hogy az újdonsült mini alkalmazásunk elindult? Mint minden rendes ASP.NET alapú alkalmazásnál a vizsgálódást a projektünk gyökerében található Global.asax fájllal kell kezdeni. Ennek a fájlnak pontosan az a szerepe, mint az ASP.NET Web Forms alkalmazásoknál, egy HttpApplication leszármazott példányt határoz meg, ami nem más, mint a mi alkalmazásunk. Ebben lehetőségünk van alkalmazás szintű eseménykezelők írására. Ilyen eseménykezelők az első request megérkezésétől az alkalmazásunk leállásáig különböző lehetőségeket adnak, hogy olyan kezelőket írjunk, amik az adott helyzetben megváltoztathatják vagy kiegészítik az alapértelmezett viselkedést, esetleg környezeti értékeket állítanak be. Ezek ugyan eseménykezelők, de nem úgy, mint a Windows Forms-ban használatos eventek. Nem kell sehova sem feliratkozni, hogy lefussanak. A metódusok szabványos elnevezésük alapján kerülnek meghívásra. Ezek az eseménykezelők opcionálisak. Viszont ahhoz, hogy
1-73
5.2 A kontroller és környezete - Az alkalmazás kiindulási pontja. A global.asax 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.
1-74
5.2 A kontroller és környezete - Az alkalmazás kiindulási pontja. A global.asax 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
1-75
5.2 A kontroller és környezete - Az alkalmazás kiindulási pontja. A global.asax 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.
1-76
5.3 A kontroller és környezete - Routing
5.3. Routing Már volt szó arról, hogy a request egy kontroller egy metódusát célozza meg az URL alapján (és nem egy fájlt, mint az a webszerver normál viselkedése lenne). Most arról lesz szó, hogy hogyan működik ez a mechanizmus. Menjünk vissza a kályhához, és vegyük megint elő a global.asax-ot. Nézzük meg a route konfigurációt: RouteConfig.RegisterRoutes(RouteTable.Routes); Ami semmit sem mond, tehát menjünk tovább az App_Start mappában levő RouteConfig.cs –hez, mert itt van az implementáció lényegi része. public class RouteConfig { public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}", defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional } ); } }
9. példakód
Ez egy statikus metódus, tehát a hasznos kód lehetne akár az Application_Start-ban is. Az MVC előző verzióiban (1-2-3) ugyanis ott is volt. Ezért is volt az előző körutazás, hogy meg tudjam mutatni, hogy ez a kódcsoportosítás az MVC 4 egy új színfoltja. Ugyanígy az App_Start mappában találhatjuk meg a FilterConfig, AuthConfig, stb. osztályokat, egyszóval az alkalmazás induláskori paraméterezéseit. A metódus bemeneti paramétere egy gyűjtemény. Ehhez a gyűjteményhez tudunk hozzáfűzni újabb route bejegyzéseket. A gyűjteménybe kerülő bejegyzések sorrendje fontos, a sorban elől levőket előbb is értékeli ki az MVC. Ha a sorban egy elemet megfelelőnek talál, akkor a továbbiakkal nem foglalkozik. Az első illeszkedő lesz a győztes. Volt már szó arról, hogy az URL domain név utáni szakasza alapján határozódik meg a kontroller és az action. Ismétlésként a /Home/Contact a HomeController.Contact() metódusát jelenti pont a fenti definíció miatt. Kicsit továbblépve lehetséges az is, hogy a Contact metódusnak rögtön egy paramétert átadjak, ha ezt írom /Home/Contact/1 és ha a Contact metódus szignatúrája 22 ilyen: public ActionResult Contact(string id)
az MVC a /Home/Contact/1 URL végén levő 1-et az ’id’ paraméterben átadja a metódusnak. Nézzük akkor a 9. példakódot. A route definíciónak van egy neve, „default”, ami most nem lényeges, de attól hogy ’default’, még nem lesz alapértelmezett. Ez csak a neve, lehetne ’akármi’ is. Ha több route bejegyzésünk van, akkor a speciálisak előre az általánosabbak hátra kerüljenek a sorban. Így a ’default’ értelmű a legutolsó legyen. A definíciónak van egy URL mintája „{controller}/{action}/{id}”, amit úgy kell értelmezni, hogy az URL-t a ’/’ jelek mentén szakaszokra bontjuk és a szakaszok egymás után controller-t, action-t és id-t jelentenek. Ha az URL ráillik erre a mintára, akkor az MVC számára világos lesz, hogy ezt a route mintát kell használnia ahhoz, hogy megtalálja a kontrollert és annak az action metódusát, és találjon az action metódushoz id paramétert. A '/' jel nem kötelező érvényű, de ez tekinthető általánosnak az "URL, mint bejárási út + erőforrásnév" séma alapján. Lehetne használni akár
22
a metódusnév, a paraméter lista típusosan értelmezve és a visszatérési típus és együtt
1-77
5.3 A kontroller és környezete - Routing kötőjelet is, sőt vegyesen is. A '/' jelre itt inkább úgy érdemes gondolni, mint az URL minta statikus szakaszára, ami nem vesz részt a kontroller, action, paraméter kiválasztásában. A MapRoute-nak van még egy "defaults" paramétere is. Ebben azt tudjuk meghatározni, hogyha az URL nem teljes, de az eleje egyébként ráillene a mintára, akkor mit helyettesítsen be a hiányzó szakaszba. Emiatt van az, hogyha elindítjuk az alkalmazást, akkor a böngészőben egy ilyesmi URL-t láthatunk: http://localhost:9999, de ugyanez az oldal fog megjelenni, ha a http://localhost:9999/Home vagy ha a http://localhost:9999/Home/Index URL-t írjuk. A 'Home' mint az alapértelmezett kontrollernév és az 'Index', mint az alapértelmezett action név. Még nem volt szó a RegisterRoutes első soráról: routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
Mint a neve is mondja ez egy kivétel, azaz ha az URL mintája ráilleszthető a „{resource}.axd/{*pathInfo*}” szabályra, akkor azt az MVC visszadobja, hogy foglalkozzon vele inkább az ASP.NET motor. Az ASP.NET alatt az .axd kiterjesztésű fájlok - az un. HTTP handlerek - egy lefordított kódban állítják össze a requestnek megfelelő teljes response csomagot. Például a paramétereknek megfelelő képet.
Route mapping saját célra Bővítsük a route bejegyzéseket egy új elemmel, „Kategoriak” néven. public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.MapRoute(name:"Kategoriak", url: "{controller}/{action}/{category}/{id}", defaults: new { controller = "Home", action = "Index", category = UrlParameter.Optional, id = UrlParameter.Optional }); routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}", defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }); }
Ezzel azt tudjuk elérni, hogy a friendly URL továbbra is jól olvasható maradjon, és ne kelljen '?',' =' és '&' jeleket bevezetni az URL-be, viszont az id mellett még a "category", mint metódus paraméter kapjon értéket az URL-ből. Ehhez természetesen az index metódus paraméter listáját meg kell változtatni ilyesformára: public ActionResult Index(string category, string id) { ViewBag.Message = String.Format("Kategória: {0} Id: {1}", category, id); return View(); }
Ha ezek megvannak, akkor a /Home/Index/Butorok/12 URL végződéssel (URL path) megnyitott oldalunk fejléce így fog kinézni:
1-78
5.3 A kontroller és környezete - Routing A ViewBag.Message tartalmát a Views/Home/Index.cshtml elején jeleníti meg ez a sor:
@ViewBag.Title.
@ViewBag.Message
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 /webshop/Home/Index/Vilagitas/16 /Home/Index/100
Eredmény Kategória: Vilagitas, Id: 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:
1-79
5.3 A kontroller és környezete - Routing /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 = @"^\d+$", category = @"(Butorok|Textil|Vilagitas)" } );
A fenti példa szerint a route definíció csak akkor érvényes, és csak akkor kell számításba vennie a Route rendszernek, ha az "id" szakasz legalább egy karakterből álló szám és a "category" helyén levő URL szakasz tartalma a | jellel elválasztott szavak egyike. Bármilyen értelmes kifejezést megadhatunk, de csak az adott nevű route szakaszra. Több paramétert egyszerre érintő megszorítást így nem tudunk meghatározni. De hogy ilyen esetben se kelljen sokat ügyeskedni, lehetőség van definiálni egy IRouteConstraint interfészt megvalósító osztályt, amiben úgy vizsgáljuk a bejövő URL szakaszokat, ahogy csak akarjuk.
1-80
5.3 A kontroller és környezete - Routing 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" } );
1-81
5.3 A kontroller és környezete - Routing
A namespaces megszorítás, akkor fog hasznunkra válni, ha az alkalmazásunk modulárisan épül fel, amikor a kontroller nem szükségszerűen a fő projektben van definiálva, hanem egy külső dll-ben. Bevallom őszintén a fejlesztés route definiálási szakaszában számos meglepetésben volt már részem, ezért is kerülöm a sok bejegyzést. Célszerűnek tartom, hogy a lehető legkevesebb definíciót készítsük el. Azonban ha erre nincs mód és több route mappelést kell meghatározni, akkor a route bejegyzéseket már a kezdeteknél lássuk el megszorításokkal. Ilyen helyzetben már érdemes tesztelni is a route bejegyzéseket, erre több eszköz is létezik. Személyesen a Glimpse 23 zseniális NuGet csomagját ajánlom. Ebben külön "Routes" nevű fül áll rendelkezésre az URL alapján kiértékelődött, aktív-inaktív route bejegyzések tesztelésére. Az alábbi képen a http://localhost:18005/Home/Index/100 -re adott elemzést láthatjuk:
A nagy zöld sáv jelenti, hogy a "Default" route határozta meg a route szabályt. A piros nyíllal jelzett sor mutatja, hogy az előbb definiált MultiConstraint osztály kiértékelési eredménye: false. A Glimpse csak akkor működik, ha a bejövő requestnek volt normál eredménye, így ha az URL és a route alapján nincs elérhető kontroller és action, akkor ez sem fog segíteni. Érdemes a többi képességét is áttanulmányozni, mert sok esetben jobb megoldást nyújt, mint egy sziszifuszi debuggolás. Végezetül az említett ajánlás még egyszer: A rendszer működése miatt ajánlatos a modellosztályunkon a route mintában szereplő szakaszokkal azonos nevű propertyk mellőzése. Így nem tanácsos 'controller', 'action', és az előbbi példát alkalmazva a 'category' property neveket definiálni, függetlenül a kis és nagybetűs változatoktól, egy ilyen route szakaszdefiníció esetén: {controller}/{action}/{category}/{id}",
Ennek okára majd a beérkező request feldolgozásánál még visszatérünk. Addig sem kell megijedni, lehet használni ezeket a neveket is csak egyes ritka esetekben nem várt eredményt kaphatunk. A route bejegyzések célja, hogy az MVC infrastruktúrája meg tudja határozni, hogy melyik kontroller melyik actionjét kell használnia. Néha azonban ez nem elég.
23
http://getglimpse.com/
1-82
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
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
5.3 A kontroller és környezete - Routing 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.
1-84
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 van: @Html.RouteLink("Details4","categories/{categ}/Details/{id:int}/{defaulvalue=Alapertek}/{notanoption}", new {id=111}) //Hivatkozás route mintára, mint névre @Html.RouteLink("Details6 opc.","Details6Route", new {categ="Cipők", id=111}) //Hivatkozás route névre
Előfordul, hogy összetett műveletet szeretnénk elvégezni, vagy space-eket akarunk tenni az egysoros kódba, ezt normál zárójellel tehetjük ezt meg. Az előbb látott (Oldalcím4), nem létező property problémát is meg lehet ezzel oldani. A @(…) zárójelekkel egyértelműen jelezhetjük a kód határát. A közrezárt kódnak szükséges, hogy legyen olyan visszatérési értéke, amit HTML kimenetté lehet alakítani. Egysoros, több utasításos szakasz: @("Az aktuális URL: " + Request.Url) @(Request.Browser.Browser).Nem létező property
Az első sor mutatja, ha email címet akarunk kiíratni, amibe a "@" jel is kell (egyébként ne tegyünk soha így). Egy kukac megjelenítéséhez írjunk kettőt. Email cím: [email protected] Egy db kukac: @@
A több sornyi C# kódot kódblokkba tudjuk tenni, amit { } közé kell írni. @{ //Kódblokk C# nyelven Write("Közvetlenül írok a kimenetre "); WriteLiteral(" <em>Közvetlenül írok a kimenetre HTML kódot is<em/> "); }
A Write metódus szöveget vár, amit viszont okosan HTML-é kódolja, emiatt a szövegbe írt „ ” nem HTML-ként kerül a böngészőhöz. Ezért van a sor végén olvasható formában. A WriteLiteral nyersen írja ki a kimenetre a paraméterben kapott stringet. Amikor kódblokkon belül nem HTML tagek közé zárt szöveget írnánk ki, akkor gondban leszünk, mert az nem értelmezhető C# kódként. Ezt egy sor esetén a „@:” markerrel tehetjük meg. Több sort a 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. A szövegbe tudunk kódot beékelni, mindegy, hogy HTML vagy normál szöveg (ld. Guid-os sorok). Érdemes megfigyelni, hogy a NewGuid() metódushívás utáni lezáró zárójelet szövegesen fogja megjeleníteni, hasonlóan a @Guid előttihez. @if (1 == 1)
1-140
6.6 A View - A View nyelvezete { //Kódblokk feloldása @:Kódblokkon belüli, egysoros, nem html szöveg
Kódblokkon belüli, html szöveg -> nem kell feloldás
@:Nem html szöveg kóddal (@Guid.NewGuid()) Html kóddal (@Guid.NewGuid()) Kódblokkon belüli, több soros, nem html szöveg feloldása . . @:{ Kapcsos zárójelek között } }
A vezérlési szerkezeteknél (if, else, for, while, foreach) a C# fordító megengedi, hogyha azt csak egysoros utasítás követi, akkor a nyitó és záró kapcsos zárójeleket elhagyjuk és elég az egészet egy darab pontosvesszővel lezárni. if (valami == null) continue;
Ez nem működik a razor esetén, ezért kénytelenek vagyunk a kapcsos zárójeleket kiírni még az alábbi egy kulcsszavas kód esetén is: @if(valami == null) { continue; }
Az olyan esetben, amikor csak két érték közül kell választani, az if+else szerkezet helyett használhatjuk a '?' operátort is:
Természetesen ilyenkor nem használhatunk csak egy @-ot mindenképpen a @( ) formulát kell igénybe venni. Az MVC 4 újdonsága, hogy ha egy property értékét egy HTML tag attribútumába írjuk, akkor azt értelmesen kezeli. A következő példában a ViewBag.CssOsztaly-nak szándékosan nem adtam értéket, tehát az null. A div1 id-jű div class definíciója a HTML kódban az lesz, hogy
és nem „foosztaly null” A div2 class és mindegynevu definíciója el fog tűnni, mindössze ez marad:
. Ú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.
Div1
Div2
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.
1-141
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.
Div3
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. @{ViewBag.isChecked = true;}
Egy kis ínyencség következik. Vajon mi lesz ennek az input definíciónak a renderelt HTML eredménye?
Ha az isChecked egy boolean true érték: Ha az isChecked egy boolean false érték: Ilyen definíciót azért elég ritkán kell készíteni, de az érem másik oldala, hogy nem pontosan azt kapjuk, amit várnánk. A megjegyzést így kell megadni: @*Kommentezett sorok ...*@
Gyakran előfordul, hogy a javascript kódba megjegyzést teszünk. Hacsak nem az a célunk, hogy a böngésző forrás nézetében is olvassák a megjegyzésünket, akkor a JS megjegyzéseit a második sor (variable2) razor komment módjával érdemes írni. Ha már a .cshtml fájlba írtuk a <script> blokkot talán így jobb. <script type="text/javascript"> var variable1 = 'abc'; //abc -> variable1-be var variable2 = 'abc'; @*abc -> variable2-be*@
1-142
6.6 A View - A View nyelvezete
1-143
Az előző sorok egyesített eredménye, alatta a generált HTML kód vége a div1 -től:
Div1
Div2
Div3
<script type="text/javascript"> var variable1 = 'abc'; //abc -> variable1-be var variable2 = 'abc';
14. példakód
A Razor nem csak egy szintaxis, hanem vannak speciális funkciói is. A @model, @section funkciókkal már találkoztunk. Mielőtt folytatnánk, meg kell nézni a View lelkivilágát is. Most egy kis mélyvíz és utána újra egy kis Razor ismertető (pihentető) következik.
6.6.2. Kód a View-ban Ha a kérdés az, hogy milyen adatokat tudunk elérni a View-ban levő kódból, a rövid válasz, hogy mindent, ami az oldal eddigi életciklusában elérhetővé vált. Ahogy a kontroller adat kontextusánál már láttuk, itt is számos property van kivezetve a View példányra. Álljunk is meg egy pillanatra. Mi az, hogy View példány? A View-ról eddig azt mondtam, hogy ez egy HTML oldal, dinamikus és statikus tartalomszakaszokkal. Ezt nem lehet példányosítani, mert csak osztályt lehet. Láthattuk, hogy a View belső szerkezete minden csak nem osztály definíció. Ennek ellentmondva azt tapasztalhatjuk, hogy a View osztály példánya az objectből származik. Ezért van neki GetType() metódusa is: @{ Type viewTipusa = this.GetType(); }
Nálam a viewTipusa ez volt: ASP._Page_Views_nezet_ViewContext_cshtml
6.6 A View - A View nyelvezete Jöjjön a magyarázat: Az action metódusból kilépve egy ViewResult-ot adunk vissza a View() metódus meghívásának eredményével, vagy közvetlenül példányosítunk egy ViewResult-ot. Ez, mint egy alkatrész csomag átkerül az MVC-hez, amiből az meghatározza a View fájlt és ezt a fájlt osztállyá transzformálja. Utána az osztályt pedig le is fordítja CIL kóddá és beleteszi egy assembly (dll) fájlba. Ez elég meleg nem? Nagyon hatékony módszer, mivel a View-t csak egyszer kell „lefordítani” utána a kész dll fájl tárolható és használható. A legközelebbi request érkezésekor már készen áll egy mini assembly. Az ebben levő osztályt csak példányosítani kell és indítani. Ez a magyarázata annak, hogy a View első felhasználása (indítása) miért sokkal lassabb, mint a rákövetkezők. A View szöveges fájljának állandó újraértelmezése rendkívül erőforrás-igényes lenne. Járjunk egy kicsit utána! Az action legyen egy lényegtelenül egyszerű kód: public ActionResult ViewContext() { return View(); }
A View a saját típusinformációit írja ki: @{ Type viewTipusa = this.GetType(); }
ViewContext felfedése
A View típusa: @viewTipusa A View dll fájlja: @viewTipusa.Assembly.Location
A futás eredménye nálam ez volt: A View típusa: ASP._Page_Views_ViewTest_ViewContext_cshtml A View dll fájlja: C:\Windows\Microsoft.NET\Framework\v4.0.30319\Temporary ASP.NET Files\root\3db22504\59dcd6a3\App_Web_0ejbcrcy.dll
A lefordított assembly-t, ezen az irgalmatlan elérési úton lehetett megtalálni. Gondolom nyilvánvaló, hogy a mappák és fájl nevének kódolt neve dinamikusan van meghatározva, tehát az MVC alkalmazást egy másik gépen futtatva biztosan más lesz a fájl és az elérési út is. Az assemblyben nem csak az aktuális View kódja van, hanem az azonos mappán belüli többi View is belekerül. Ez egy előtakarékosság. Az assembly fájl nevével egyezően ebben a mappában még ott vannak a C# forráskódok is. Megkerestem a konkrét View forrását, és kigereblyéztem belőle, ami számunkra most nem fontos. A lényege úgy is az, hogy a View-ba írt szöveg string formában kerül kiíratásra. A View elején levő típusinformációkat kiíró kód pedig úgy van ott, az Execute metódusban, ahogy azt megírtam. public class _Page_Views_ViewTest_ViewContext_cshtml : System.Web.Mvc.WebViewPage { 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
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("\r\n\r\n \r\n <meta"); //... WriteLiteral("\r\n \r\n "); //... Write(RenderSection("featured", required: false)); //... Write(RenderBody()); //... WriteLiteral("\r\n \r\n\r\n"); }
Láthatóak a „featured” section és a RenderBody generálásának az eredményeit fogja kiírni a háttérben dolgozó TextWrite-re, ami a response-t tölti majd fel. Megjegyzendő, hogy az általunk írt hagyományos kód az Execute() metódusba kerül bele. Így normál esetben csak olyan kódot írhatunk a View-ba, ami egy C# metóduson belül is megállja a helyét. Osztály-, property- vagy metódusdefiníciót nem. A nem normális esetet a @helper és a @functions kulcsszavakkal bevezetett blokkal tudjuk megvalósítani (a következő részben megnézzük…). A fentiekből következik, hogy a View futásából is ki lehet "ugrani". Elég gyakran alkalmazom a következő if-es vezérlési szerkezetformát, hogy a felesleges {{{ … }}} jellegű mély beágyazásokat elkerüljem, és kicsit rövidebb és olvashatóbb legyen a kód: Mély beágyazás
Nincs beágyazás
if (objektum != null) { if (objektum.prop != null) { //Lényegi kód } }
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) az @if(valami != null) { return; } formulával megszakíthatjuk a futását. Ez félkész View tesztelésénél, hibakeresésnél is jól jöhet. Ez a dinamikus fordítási metodika megengedi, hogy a View szöveges tartalmát futásidőben szerkesszük. Ezért nem kell újraindítani az alkalmazást, ha a View-ban változtattunk. A változást észleli az MVC és újrarendereli a View-t -> újra előállítja belőle a C# kódot és a dll-t. Amikor a Visual Studio-ban egy MVC projektet lefordítunk, akkor a View-ban levő kódot nem fogja bevonni a fordítási procedúrába, mivel arra majd az „élő műsorban” kerül sor. Tehát, ha hibát írtunk a View-ba, az is csak futásidőben fog kiderülni. Ez bosszantó tud lenni, amikor az éles alkalmazást szeretnénk telepíteni és biztosak akarunk lenni, hogy legalább fordítási hiba nem fog megtörténni később a felhasználó szeme láttára. Egy kis trükkel rávehetjük a VS-t, hogy mégis fordítsa le nekünk a View-kat is. Az MVC projektfájlt kell szerkeszteni hozzá. Ezt futó VS mellett két módon is megtehetjük.
6.6 A View - A View nyelvezete
Az első a drasztikus módszer: A projekt fájlt ( .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 .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 bejegyzést és írjuk át a „false” –t „true”-ra. Fájl mentés és a projekt betöltése után a következő fordításkor a View-k is fordításra kerülnek. Próbáljunk szintaktikus hibát írni valamelyik View C# kódszakaszba, hogy lássunk eredményt, akarom mondani fordítási hibát.
6.6.3. Razor kulcsszavak Térjünk vissza a Razor nyelvi lehetőségeihez. A szekciók és ezek beékelését megvalósító. @section-t már részletesen megismertük a _Layout tárgyalásánál. Ennek nincs C# kóddal kapcsolatos funkciója, csak szekciókat injektál a View-ból a hosztoló Layout-ba. Szintén láttuk már a @model –t, amivel a View által igényelt és használt modell típusát tudjuk deklarálni. Ezzel típusossá tudjuk tenni a View-t. Ennek a legnagyobb haszna, hogy a View Model propertyt ilyen típusként tudjuk elérni, míg ha nem használjuk a @model-t, akkor a Model property dinamikus (dynamic) típusú lesz. Amiben az a legrosszabb, hogy nincs IntelliSense támogatása. A View osztálytípusa a @model által meghatározott generikus leszármazott lesz.
Azt mondtam, hogy @model típusossá teszi a View-t. A View mellett típusosak lesznek a ViewData, a Html és Ajax helperek szintén. Emiatt tudjuk használni többek között a Html.TextBoxFor, Html.LabelFor, Html.EditorFor és az összes …For –végű Html helpert. Jöjjenek az újdonságok. Nagyon valószínű, hogy egyszer a View kódunkban hosszabb névterű típust is szeretnénk használni. A C# kód írásakor megszokott using itt is használható például @using System.Web.Http formában, valahol a View fájl elején. A lezáró pontosvesszőre itt sincs szükség.
1-146
6.6 A View - A View nyelvezete
1-147
A @using –ot nem érdemes használni abban az esetben, ha a névteret szinte az összes View-ban használjuk. Ugyanis a Views mappa alatt található web.config fájlban van egy szakasz, ahová további, közösen használt névtereket vehetünk fel:
A Html helperek HtmlAttributes paraméterével tudunk HTML tag attribútumokat deklarálni, egy anonymous típuson keresztül. @Html.ActionLink("Ugras a szintaxis oldalra", "Szintaxis", null, new {id="ug1"} )
A HTML tagek közül a legfontosabbal a „class”-al gondban lennénk, mert ez egy C# kulcsszó (szintén a legfontosabb). A dilemma feloldásához a C#-hoz hasonlóan, a razorban is használhatjuk a @class formát. Ennek funkciója tényleg csak az, hogy a class szót bele tudjuk írni egy anonymous típus property listájába. A használatára a sor végén látható a példa. @Html.ActionLink("Ugrás a szintaxis oldalra","Szintaxis", null, new {id="ug1", @class = "kiemelt"} )
Az ActionLink-nek egy további paramétere a RouteValue. Ebben lehet összegyűjteni a hivatkozás összes URL paraméterét (query string). Az alábbi példában csak a „honnan” URL paraméter-t adjuk meg egy anonymous objektummal. @Html.ActionLink("A 2. oldalra, URL paraméterrel", "Hlink2", "Helper", new { honnan = "Hlink" }, null)
Ebben a példában az URL paramétereket collection initcializer-el állítjuk be. Az eredmény azonos lesz. @Html.ActionLink("A 2. oldalra, URL paraméterrel RouteValue Dictionary", "Hlink2", "Helper", new RouteValueDictionary {{"honnan","Hlink"}}, null)
Mint rendes URL paraméter megjelenik a böngésző címsorában: /Helper/Hlink2?honnan=Hlink. A paramétert az action metódus paraméterként képes fogadni. Az ActionLink nagyon túlterhelt metódus. Nagyon kevés különbség van a paraméter listákban, ráadásul azok is átfedésben vannak a típusmentesség miatt. Ezért arra egy kicsit oda kell figyelni, hogy az átadott paramétert miként fogja értelmezni. Ezért látható a paraméter lista végén a null, hogy egyértelmű legyen melyik metódus változatot is hívom a példában. Megfontolandó kiírni a paraméter nevét is (routevalues:), ha bizonytalanok lennénk. @Html.ActionLink("Szöveg","Hlink2", routeValues: new { honnan = "Hlink" })
1-153
6.8 A View - Beépített Html helperek
1-154
Lehetőségünk van HTML attribútumok meghatározására is anonymous objektummal, ami egy elegáns kódformát ad. @Html.ActionLink("A 2. oldalra, de új ablakban", "Hlink2", null, new { id = "indexlink", @class = "linkek", @target = "_blank" })
RouteLink Nagyjából mindent meg lehet oldani az ActionLink-el is, de van még egy linkgenerálási lehetőség a RouteLink. Ez nagyobb site-oknál ad segítséget és lehetőséget arra, hogy több route bejegyzés közül kiválaszthassunk egyet a route map neve alapján. Ha sok route bejegyzésünk van, előfordulhat, hogy a kontroller és az action név meghatározása nem elég, mert más bejegyzésre is ráillene, ami megelőzi a kívánt sort a route listában. (a lista első illeszkedő eleme nyer ld.: 5.3 Routing fejezet). Az itt következő első példának sok hasznát nem vesszük, mert teljesen úgy fog működni, mint egy ActionLink. @Html.RouteLink("A 2. oldalra, URL paraméterrel", "Default", new { action = "Hlink2", controller = "Helper", honnan = "Hlink" }, null)
A hatás kipróbálásához fel kell venni egy új route bejegyzést: routes.MapRoute( name: "complains", url: "complains/{controller}/{action}/{id}", defaults: new { id = UrlParameter.Optional } );
Legyen ez a RouteLink paramétereivel: @Html.RouteLink("A panaszos oldalra", "complains", new { action="New", controller="Incoming"}, null)
A szövegesen megadott "complains" paraméter hivatkozik az azonos A generált tag: A panaszos oldalra
nevű
route-ra.
Látható, hogy az URL a „complains” route bejegyzés url paramétere szerint állt össze. A RouteLink sajátossága, hogy az URL-t értelmesen állítja össze. Tehát ha a megadott paraméterek szerint nem képezhető link, akkor az alkalmazás gyökeréhez készíti a linket.
6.8.3. Űrlap. BeginForm Rendelkezésre áll a HTML form generálásához szükséges Html.BeginForm metódus. AZ ismertető előtt szeretnék mutatni a tagek közti mezők tartalmát annak neveivel. Az URL lehet abszolút, azaz az URL path eleje /-el kezdődik. (http://localhost/url/path/cont vagy /url/path/cont ). Lehet relatív, ahol
6.8 A View - Beépített Html helperek a kiinduló pont az aktuális oldal URL-hez képest additív path. Leggyakrabban csak egy fájlnév vagy egy szó. Példaként a GET-el lekért oldal (és benne a form) URL-je legyen: /url/path/cont és az action attribútum mindössze csak: contback, ennek megfelelően a submitkor a /url/path/contback lesz a böngésző által előállított URL path. A HTML form másik paramétere a method. Ezzel tudjuk informálni a böngészőt, hogy a submit műveletet milyen HTTP igével küldje vissza a szervernek. Ez legtöbbször a POST szokott lenni, de mielőtt ezt kőbe véssük, elmélkedjük tovább. Lehet, hogy ez az ASP.NET-hez (és más környezethez) szokott fejlesztő Pavlovi reflexe, de nem biztos, hogy jó minden esetre a POST. Egy nézőpontból értelmezve a HTML formok felhasználási esetei két csoportba oszthatók: 1. A felhasználói input eredménye az, hogy a szerveren létrejön vagy megváltozik egy DB entitás. Ezek a tipikus felhasználói űrlapok amit kitöltetünk, majd az eredményét eltároljuk. Utána a felhasználót egy másik oldalra irányítjuk, ahol megköszönjük a vásárlást és megmutatjuk a számla végösszegét, hadd ájuldozzon. Ez nyilvánvalóan POST method-al szokott zajlani, aminek több oka is van. Ennél a megoldásnál valóban fontos, hogy másik oldalra irányítsuk a böngészőt, mert ha nem, akkor a felhasználó azt fogja hinni, hogy nem jól töltötte ki az űrlapot. A form adatok nem lesznek láthatóak, mert a HTTP csomagba lesznek benne. 2. A felhasználói input nem hoz létre és nem változtat meg semmilyen lényeges üzleti entitást az űrlap mezői alapján, legfeljebb naplózzuk amit beírt a felhasználó a mezőkbe. Ezek a tipikus keresési, szűrési feltételek űrlapjai. Itt megadja a felhasználó a méretet, a színt, az árkategóriát, stb. ennek eredményeként megmutatjuk neki a szűrési feltételeknek megfelelő terméklistát. Erre viszont nem annyira jó a POST, legfeljebb akkor, ha a szűrési feltételek nagyon sok szempontból állnak (>10). Ha a keresési feltételek input mező adatait GET method-al küldjük vissza, akkor a keresési oldal URL-jében megjelennek a feltételek URL paraméterek formájában, amit a felhasználó ki tud másolni és pl. emailben tovább tud küldeni a kollégájának, barátjának. Ezzel megkíméli őt a szűrési feltételek újra beállítgatásától, vagy akár be tudja rakni a kedvencek közé. A felhasználók nem biztos hogy mindig úgy használnák a rendszerünket, ahogy mi azt elképzeljük. Ilyen pici POST->GET szemléletváltás jelentős előnnyel járhat számukra. A HTML formból minkét paramétere elhagyható, ekkor a form action az aktuális oldal URL-je, a method pedig a GET lesz. Arra azonban nem vennék mérget, hogy minden helyzetben pl. karórában, mikro sütőben futó böngészőben is működni fog, ezért legalább a form actiont érdemes lesz megadni. A Html.BeginForm nagyon hasonlít az ActionLink-re a paraméterei tekintetében, mivel ez is URL-el operál. Szintén meg lehet adni RouteValues-t és HTML attribútumokat is. Azonban van egy furcsasága, mivel a tag-ek közé szövegek és HTML elemek kerülnek, így ezt nem lehet definiálni egy darab HTML tag generálásával, ehhez kettő is kell. Emiatt van a Html.BeginForm mellett Html.EndForm metódus is. A bevált gyakorlat azonban az, hogy a BeginForm-ot using blokkba tesszük. Úgy trükköztek a framework készítői, hogy a BeginForm statikus metódus egy MvcForm objektumot ad vissza, ami IDisposable. Amikor a @using {..} blokknak vége, a .Net meghívja a MvcForm Dispose() metódusát, ahogy egy jó using blokk végén szokás. Erre a MvcForm utolsó leheletéből odapottyant még egy lezáró 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").
1-155
6.8 A View - Beépített Html helperek
1-156
@using (Html.BeginForm("Hform", "Helper", new { id = 1 }, FormMethod.Post, new { id = "form1" })) { @:Szöveg: @Html.TextBox("Szoveges") }
A generált html:
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 tag-ek közé zárni az és <select> elemeket. Ezek rendelkeznek már a "form" attribútummal, amivel közölhető, hogy melyik formba értjük bele a szóban forgó elemet (mintha ott lenne a formon belül, de a dizájn miatt nem lehet odatenni).
6.8.4. Szövegbevitel. TextBox, TextArea A HTML űrlap önmagában mit sem ér, tehát következzenek a beviteli mezők. A kipróbáláshoz szükség lesz két actionre. Egyre, ami a meglévő adatokkal GET esetén kiszolgálja a View-t és egy másikra, ami a POST adatokat fogadja, amik alapján frissíti a tulajdonságokat a már megismert memória alapú tárolónkban. public ActionResult Hinput() { return View(ActionDemoModel.GetModell(1)); } [HttpPost] public ActionResult Hinput(int? id, FormCollection fcoll) { if (!id.HasValue) return RedirectToAction("Hinput"); var model = ActionDemoModel.GetModell(id.Value); if (TryUpdateModel(model)) { return View(model); } return View(ActionDemoModel.GetModell(id.Value)); }
15. példakód
6.8 A View - Beépített Html helperek
1-157
A form definíciója hagyományos Html helperekkel: @using (Html.BeginForm("Hinput", "Helper", new { id = Model.Id }, FormMethod.Post)) { @:Szöveg: @Html.TextBox("FullName", Model.FullName) @:Multiline @Html.TextArea("Address", Model.Address, 2, 20, null) @:Rejtett: @Html.Hidden("FullNameOrig", Model.FullName)
}
16. példakód
A TextBox egy-, a TextArea többsoros szöveges beviteli mezőt biztosít. Az első paraméterük az name attribútuma lesz, a második a kezdeti szöveg értéke, ez kerül a value-ba. A keletkezett HTML sor a TextBox alapján: A name mellett az id is felveszi a második paraméter értékét. Lehetőségünk van az id generálást megváltoztatni az id megadásával, ahogy az ActionLink-nél már láttuk, például HTML attribútumokká alakuló anonymous osztállyal. A TextArea Html helpere sem túl bonyolult. A 3. és 4. paramétere a sorok és oszlopok száma, ami el is hagyható. (a null a HTML attribútumok definíciójának a helyét áll) @Html.TextArea("Address", Model.Address, 2, 20, null)
Ott van még a Hidden helper: @Html.Hidden("FullNameOrig", Model.FullName)
Amikor a submittal beküldjük a formot, és az ActionResult Hinput(int? id, FormCollection fcoll) action fogadja azt (15. példakód). A FormCollection-ban pedig ott lesznek a HTML beviteli mezők név-érték párokban, amit most nem is használunk fel, csak a demó kedvéért van ott. Az Id-ben benne lesz az eredeti objektum id-je, mert a form RouteValue listájába beletettük. A GetModell-el elkérjük az eredeti entitást és a kontroller TryUpdateModel metódusával frissítjük az adatait. Valós helyzetben, ez után következik még egy adatbázis update, de itt nincs rá szükség. A TryUpdateModel hívásával a model binder-t aktivizáltuk, ami a háttérben a beviteli mezők neveivel összepárosítja a modell propertyket a neveik alapján. Ha az adat érvényes, akkor felülírja a modell propertyk adatait. A model binder egyik alapvető funkciója, hogy az action hívása előtt automatikusan beindulva, az action metódus paramétereit be tudja állítani a HTTP post adatok alapján. Emiatt írhattuk volna így is az action metódust: [HttpPost] public ActionResult Hinput2(int? id, string FullName, String Address) { if (!id.HasValue) return RedirectToAction("Hinput"); var model = ActionDemoModel.GetModell(id.Value); model.FullName = FullName; model.Address = Address; return View("Hinput", model); }
6.8 A View - Beépített Html helperek 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 is: @using (Html.BeginForm("Hinput", "Helper", new { id = Model.Id }, FormMethod.Post)) { @:Szöveg: @Html.TextBoxFor(m => m.FullName) @:Multiline @Html.TextAreaFor(m => m.Address, 2, 20, null) @:Rejtett: @Html.HiddenFor(m => m.FullName, new { name = "FullNameOrig", id = "hnev" }) @:Jelszó: @Html.PasswordFor(m=>m.Address, new {Name = "Jelszo1"}) }
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: 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. 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 jelszó beviteli mezőt hoznak létre a szokásos pöttyökkel.
1-158
6.8 A View - Beépített Html helperek
6.8.5. Label és formázott megjelenítés Ami nem tetszik az előző példában továbbra sem, hogy egy @:Szöveg –el adtam tájékoztatást a felhasználó számára, hogy mit is írjon a mezőbe, holott rendelkezésre áll a propertyhez tartozó Display attribútumban a felirat. A HTML