5. gyakorlat¶
MVC bevezetés¶
Az MVC architekturális model az egyik legegyszerűbb mind közül. 3 jól elkülöníthető réteget definiál melyek:
- Model (M)
- View (V)
- Controller (C)
A rétegek közül a felhasználó a View-n keresztül kommunikál. A felhasználói interakció valamilyen eseményeket generál, melyre a Controller a megfelelő hívásokat ejtheti meg. A Controller tekinthető egy középső rétegnek, mert a Model réteget csak ő képes adatmódosítási céllal elérni. Tehát a View-ból például sosem kezdeményezünk adatbázis műveleteket, mert az veszélyes lenne az alkalmazás szempontjából, illetve a View-nak nem is kell tudnia arról, hogy az adat honnan jön és hogyan van tárolva. Számára az a lényeg, hogy tudja hogy milyen adatokat kell megjelenítenie a felhasználó számára.
A Controller-ben végzünk minden üzleti logikával kapcsolatos tevékenységet. Például bizonyos mezők értéke alapján egyéb értékeket állíthatunk be automatikusan, melyet már a felhasználó nem lát.
A Model réteg több dolgot foglalhat magában. Egyrészt a Bean osztályainkat rendre itt szoktuk létrehozni (A Bean definícióját lásd később). Másrészt az úgynevezett DAO is itt szokott helyet foglalni, mely az adatelérésért felelős, legyen az egy adatbázisban vagy egy fájlban.
Ezen felül a View rétegnek nyilván szüksége lehet magukra a Bean-ekre is, ezért ő használhat ilyen jellegű olvasási műveleteket közvetlenül a Model rétegből.
Az imént leírtak grafikusan szemléltetve:
MVC alkalmazás implementálása¶
Feladat
Készítsünk egy egyszerű címjegyzék alkalmazást!
A címjegyzékbe az alábbi funkcióknak kell elérhetőnek lennie:
-
Új kontakt hozzáadása, melynél a következő információkat kell lehet megadni:
- Név (Kötelező)
- Email (Kötelező)
- Telefonszám (Opcionális) - lehet több is és meg lehet adni a típusát (work, home)
- Lakcím (Opcionális)
- Születésnap (Kötelező)
- Szervezet (Opcionális)
- Pozíció/Beosztás (Opcionális)
- Kontakt szerkesztése
- Kontakt törlése
- Kontaktok listázása
-
Kontaktok közötti keresés a következők alapján
- Név
- Export és import funkciók támogatása (vCard), melyhez tetszőleges 3rd party lib használható
Maven Multimodule¶
Maven segítségével létrehozhatunk úgynevezett multi-module projekteket is, ahol van egy parent pom, mely a projekt gyökerében található és és a package-elése POM-ra van állítva.
Ebben a parent pom-ban, melyet aggregator POM-nak is szokás nevezni, megadjuk a module-okat, melyek az eddig látott projektektől semmiben sem különböznek, csupán meg kell adnunk a <parent>
elemek között a parent POM-ot.
Mielőtt a konkrét példát megnéznénk, vegyük sorra, hogy milyen előnyöket szolgáltat a számunkra ennek a konstrukciónak a használata:
- Duplikációk csökkentése, mivel a modulokat több helyen is felhasználhatjuk
- Közös konfigurációk kiszervezése a parent POM-ba (vagy más néven Super POM-ba)
Címjegyzék projekt létrehozás¶
Készítsünk egy új projektet (File -> New -> Project
), melynek típusát Maven-re állítsuk!
A nevének adjuk a contacts
-ot, illetve groupId
-nak használhatjuk a hu.alkfejl
elnevezést!
A létrejövő projektben az src
mappa törölhető is, mivel itt majd csak almodulokat hozunk létre!
Ezután a projektre jobb klikkelünk és a New-> Module
opció alatt szintén Maven projektet adjunk meg.
A következő oldalon a Parent
értékét már fel is kínálja, hogy legyen az a contacts
.
Ezután adjuk neki a contacts-core
nevet!
Ennek eredményeképpen a Project View
fülön a contacts
alá kerül be a contacts-core
, mely saját pom.xml
-el rendelkezik.
A contacts/pom.xml
tartalma a következő lehet:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
Vegyük észre, hogy a parent POM package-elése pom
lett, illetve a contacts-core
megjelenik a modulok alatt (ami a buildelés miatt fontos).
Még egy dolgunk lehet, hogy a közös property-ket csak a parent pom.xml
-ben adjuk meg (Pl.: <maven.compiler.source>11</maven.compiler.source>
), így azt a submodule majd úgyis örökli (persze ettől függetlenül a submodule bármikor felülírhatja a megadott property-ket).
A contacts/contacts-core/pom.xml
ezután valahogy így nézhet ki:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
A projekt package-elését nem muszáj jar
-ra állítani, mivel ez az alapértelmezett, de a jelenlegi célunk nyomatékosítása végett most megadtuk.
A fontos rész a parent
elemek között található, ahol megadjuk a hivatkozást a parent pom-ra.
Vegyük észre, hogy jelen esetben nem kell megadnunk a groupId
-t és a version
-t, mivel a submodule ezeket a parent pom-ból átveszi az effective pom-ba.
Ezután ha kiadunk egy mvn package
parancsot a parent könyvtárában, akkor az összes a modules
elem alatt megadott submodule-t buildeli a rendszer (függőségeket is figyelembe veszi).
Model réteg¶
A model osztályok lesznek azok, akiket mind asztali, mind webes környezetből szeretnénk majd használni, így azokat a contacts-core
submodule-on belül hozzuk létre.
Bean osztályok¶
Készítsünk egy hu.alkfejl.model
package-et, melyben létrehozunk egy Contact.java
állományt!
Ez egy úgynevezett Bean
osztály lesz, melynek néhány fontos tulajdonsággal rendelkeznie kell.
Ez esetünkben kevésbé lesz fontos, viszont a keretrendszerek (pl. ORM-ek, mint például a Hibernate) ezeket masszívan használják, így követendő példa már most a megfelelő módon előkészítenünk alkalmazásainkat.
3 tulajdonsággal kell rendelkeznie egy Bean
osztálynak:
- Van publikus default konstruktora
- Minden adattaghoz tartozik publikus getter/setter (másnéven accessorok)
- Az osztálynak szerializálhatónak kell lennie
Az első kettőt talán nem kell elmagyarázni, de mi is az a szerializálhatóság?
A Java rendelkezik az úgynevezett objektum szerializálással, ami szerint egy objektum reprezentálható egy bájt stream-mel, mely tartalmazza az objektum típusát, illetve hogy maga az objektum milyen típusú fieldekkel rendelkezik, továbbá a konkrét adatot is ezekhez a fieldekhez.
Másszóval a bájtstreamben benne van az objektum aktuális állapota (melyik field milyen értéket tárol).
Amennyiben ezt a bájtfolyamot valahova elmentjük ezt nevezzük szerializációnak.
Ez megfelel az objektum aktuális állapotának elmentésével.
Például lementhetjük egy objektum állapotát ilyen módon egy fájlba is, de például az is szerializáció, amikor egy objektumot JSON formára alakítunk (közkedvelt keretrendszer a Jackson ennek használatára).
Amikor az elmentett állapotot visszatöltjük (a memóriába), akkor deszerializációról beszélünk.
Ahhoz, hogy egy osztály objektumai szerializálhatóak legyenek egyszerűen meg kell valósítanunk a Serializable
interface-t.
Esetünkben egyelőre nem fogjuk alkalmazni az interfészt.
Amint szükségünk lesz rá, akkor azt úgyis könnyen megtehetjük.
Mivel a model osztályainkat property-k használatával szeretnénk elkészíteni, így szükségünk lesz egy függőségre, melyet a contacts/contacts-core/pom.xml
-ban adunk meg:
1 2 3 4 5 6 7 |
|
A property-k a javafx-base
modulban találhatóak, így elég ezt a dependency-t használnunk (ne felejtsük el frissíteni a Maven modulokat).
Ezek után lássuk a Contact.java
bean osztályunkat!
1 2 3 4 5 6 7 8 9 10 11 12 |
|
A StringProperty
-ket már láttuk, viszont érdemes megnézni a telefonszámok tárolását!
A telefonszámokat egy saját Phone
osztályban adjuk meg, ahol a telefonszámoknak van egy PhoneType
-ja (pl.: otthoni vagy munkahelyi), illetve maga a telefonszám (hamarosan látjuk a részleteket).
Mivel több telefonszámot szeretnénk tárolni, így azokat egy ObservableList<>
-ben fogjuk megadni, melyet szintén elhelyezhetünk egy ObjectProperty
-ben.
A Date
osztályhoz nincs hozzá tartozó property osztály, így azt ObjectProperty
-ként tudjuk tárolni.
A könnyebb kötések kialakítása végett a java.util.Date
helyett a java.time.LocalDate
osztályt használjuk, mivel a felületen használni kívánt DatePicker
is LocalDate
-et használ belül property-ként.
Megjegyzés
A Lombok projekt egy olyan library, ami a motorikus feladatok eliminálásával megkönnyíti a fejlesztést.
Például a getter/setter metódusokat egyetlen annotációval jelezhetjük a rendszer számára, illetve a loggoláshoz használt field-eket is egy rövidke annotációval rendelkezésre bocsáthatjuk.
A fenti Contact
osztályt a következőképpen is írhattuk volna:
1 2 3 4 |
|
A @Data
annotáció legenerálja a getter-eket és setter-eket is, továbbá paraméteres konstruktor, toString()
, equals()
, hashCode()
metódusokat is biztosít.
Az alkalmazható feature-ök listájáért lásd a Lombok weboldalát!
IntelliJ-hez elérhető a Lombok plugin, melyet telepítenünk kell., ezen felül a projekthez hozzá kell adnunk a következő Maven függőséget:
1 2 3 4 5 6 7 8 |
|
A Lombok projekt igen hasznos tud lenni, azonban a property-k esetében nem megfelelően generálja a getter/setter és XXXProperty() metódusokat, így jelen esetben nem használjuk.
A Phone
osztály a következőképpen nézhet ki:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
|
Mivel a PhoneType
szorosan kapcsolódik a Phone
-hoz, így az ezt reprezentáló enum
-ot a Phone
osztályon belül adjuk meg, mely első körben 4 féle értéket ad meg lehetségesként.
Magát a PhoneType
-ot is egy ObjectProperty
-ként tartjuk számon.
Data Access Object (DAO)¶
Miután elkészültek a bean-ek, elkezdhetünk dolgozni a DAO-n. A DAO (Data Access Object) az adatelérési réteget adja meg, mely a model rétegen belülre sorolható. A konkrét hozzáadás és listázás itt történik (mármint az adatok tárolása, legyen az memória, adatbázis, fájl vagy bármi egyéb). A controller rétegünk majd ezt fogja felhasználni. Fontos, hogy itt tartsuk mindig szem előtt, hogy interface mögé rejtsük az aktuális implementációt, mely biztosítja, hogy könnyen cserélhető legyen a megvalósítás.
Először készítsük el a jól definiált interface-t!
Ehhez készítsünk egy új interface-t ContactDAO
néven a hu.alkfejl.dao
csomagba!
1 2 3 4 5 6 7 |
|
Első körben 3 implementálandó metódussal rendelkezik, melyek már felhasználják ez előzőleg definiált bean osztályunkat.
A 3 megadott metódus lenyomat az összes kontakt listázásához, egy kontakt mentéséhez/módosításához és a törléshez lesz majd használható.
A hozzáadásnál fontos, hogy visszaadunk egy Contact
értéket is, melynek szerepe, hogy a controller felé jelezze, hogy sikeresen megtörtént a beszúrás vagy valami hiba végett ez meghiúsult (ebben az esetben adhat vissza null
-t is akár).
Nyilván nem túl szofisztikált azt közölni a felhasználóval, hogy 'Valami hiba történt', mivel a felhasználó általában arra is kíváncsi, hogy mi volt a hiba oka.
Erre most nem térünk ki, de bővítési lehetőségként mindenki elkészítheti saját maga számára.
Miután megvan az interface, el kell készítenünk egy tényleges implementációt is hozzá.
Ehhez készítsünk egy osztályt az interface mellé ContactDAOImpl
néven.
A megvalósításunk jelen esetben egy SQLite adatbázist fog használni, ugyanakkor a konkrét adatbázis nem jelenik meg a kódban, hiszen a JDBC - Java Database Connectivity API-t használjuk.
A JDBC egységes interfészt ad a különböző adatbázisok kezeléséhez, melyeket akár kombinálhatjuk is egy alkalmazáson belül.
A konkrét megvalósításokat, melyeket az adatbázis gyártók (vendorok) szolgáltatják, a JDBC DriverManager
osztályán keresztül regisztrálhatjuk.
A DriverManager
továbbá egy Connection
factoryként is funkcionál, azaz tőle tudunk adatbázis kapcsolati objektumokat kérni a getConnection()
factory metóduson keresztül.
A JDBC segítségével mind DDL (Data Definition Language) és DML (Data Manipulation Language) parancsokat is kiadhatunk, továbbá tárolt eljárásokat is meghívhatunk.
Az SQL utasításokhoz a JDBC a következő osztályokat biztosítja:
- Statement: Egyszerű paraméter nélküli utasításokhoz. Például
SELECT * FROM CONTACT
. - PreparedStatement: Paraméteres lekérdezésekhez. Például:
SELECT * FROM CONTACT WHERE id = ?
, ahol azid
értékét a valamilyen tetszőleges értékre állíthatjuk majd be. - CallableStatement: Tárolt eljárások használatához/meghívásához. Például:
{call proc_name(?,?)}
.
Ezek közül a későbbiekben az első kettőt fogjuk használni. A JDBC továbbá támogatja a tranzakciókezelést is, de ezzel a kurzuson nem foglalkozunk.
Magához a JDBC használatához nem kell semmilyen függőséget sem megadnunk, hiszen a JDBC részét képezi a Java SE-nek, azaz benne van az JDK-ban.
Viszont az adott adatbázis driver-ét biztosítani kell futásközben, így adjuk hozzá az SQLite drivert a contacts-core
függőségeihez!
1 2 3 4 5 6 |
|
Miután ezzel megvagyunk, akkor még érdemes lehet egy command line eszközt telepíteni, mellyel az SQLite-ot kezelhetjük.
Ehhez az SQLite honlapjáról töltsük le a sqlite-tools-win32-x86-3340100.zip
állományt vagy a neki megfelelő Linux-os vagy Mac-es zip-et!
Benne megtalálhatjuk az sqlite3
binárist, melynek mappáját a kicsomagolás után adjuk hozzá a PATH
környezeti változóhoz a könnyebb kezelhetőség végett.
Ezután próbáljuk ki a command-line tool-t:
1 2 3 4 5 6 7 |
|
Az alap parancs kiadásakor egy in-memory adatbázis jön létre, azaz amikor kilépünk az sqlite3-ból, akkor elvész az összes addigi adat.
Látható, hogy a .open FILENAME
parancs kiadásával használhatunk egy fájlt, mint adatbázist, melyben az adatok perzisztensek lesznek, azaz megmaradnak a leállítás után is, hiszen azt a lemezen tartósan tároljuk.
A .help
parancs kiadásával többet is megtudhatunk a command-line eszköz használatáról.
Egy igen hasznos parancs például a .read FILENAME
, mellyel külső fájlban megadott SQL parancsokat futtathatunk (például egy ddl.sql
fájlban megadjuk a DDL utasításokat, majd azokat a .read ddl.sql
paranccsal le tudjuk futtatni).
A command-line tool használatát ki is válthatjuk, ha az IntelliJ-n belül a jobb felső sarokban a Database
fülre navigálunk.
Ezután a Database fülön a bal felső sarokban szereplő + jelre kattintva egy új adatbázist adathatunk hozzá.
Ehhez válasszuk a lenyíló menüben a Data Source -> SQLite
menüpontot!
A Data Source-nak adhatunk egy tetszőleges nevet, majd a General beállításoknál a fájl alatt megadhatjuk, hogy melyik létező adatbázisfájlból dolgozunk.
Amennyiben új állományt szeretnénk használni, akkor File megadása sor végén válasszuk a + jelet és adjuk meg az adatbázis fájl helyét.
Az URL-t a JDBC-n belül is használni fogjuk majd, mivel ezzel az URL-el tudunk kapcsolatot létesíteni az adatbázis felé.
Amennyiben az IntelliJ-n belül nincs még letöltve az SQLite Driver, akkor Properties
lapon beállíthatjuk (utólag jobb klikk az adatbázis kapcsolatra és Properties
menüpont kiválasztása).
Ezután megjelenik a megfelelő nevű adatbázis a listában, amelyet lenyitva megtekinthetjük az adatbázishoz tartozó sémákat.
Itt a main
-re jobb klikk után New -> Table
menüpontot választva létrehozhatjuk a kívánt táblákat (később egy táblára jobb klikk, majd Modify Table
opcióval módosíthatjuk annak összetételét).
Miután létrehoztuk a megfelelő táblákat jobb klikk a main
sémára, majd SQL Scripts -> Generate DDL to Query Console
.
Itt láthatjuk a legenerált DDL utasításokat, mely így néz ki:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
Mivel az adatbázis elérését többször is használni kívánjuk majd, így célszerű ezt az értéket kiszerveznünk egy állományba.
Hozzunk létre a contacts-core/resources
alá egy application.properties
állományt majd adjuk meg benne a következőt:
1 |
|
Az URL-t mindenki módosítsa a saját környezetének megfelelően!
Ezután készítsünk egy segédosztályt, mely beolvassa ezt a properties
állományt, melyben a kulcs-érték párokat adjuk meg (itt bármilyen tetszőleges párokat felvehetünk, amit majd később használni szeretnénk).
Erre a célra készítsünk a hu.alkfejl.config
csomag alá egy ContactConfiguration
nevű osztályt, melynek a tartalma a következő:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
Mivel egy statikus metódusokat kínáló osztályról lesz szó, így a statikus init blokkban adjuk meg a properties fájl betöltését.
Ezután a property-ket eltároljuk egy lokális változóban, melyeket a getValue
segítségével érhetünk el.
Ezután végre elkészíthetjük a DAO megvalósítását.
Először nézzük a field-eket és a findAll
metódust!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
|
Elsőként létrehozzuk a kapcsolatot az adatbázis felé, melyet a JDBC DriverManager
osztály getConnection
factory metódusával tudunk megtenni.
Mivel az összes kontakt lekéréshez nincs szükség paraméterre, így egy sima Statement
-et használunk.
Az összes lekérdező SQL utasításhoz használjuk az executeQuery
metódust, mely az eredményt egy ResultSet
objektumban adja vissza.
Ezen osztályok mind implementálják az AutoClosable
interfészt, így használhatóak a try-with-resources konstrukcióval.
Ezután a ResultSet
-en végiglépegetünk a next()
hívással, mely mindaddig igazat ad vissza ameddig van még sor az eredmény objketumban.
A ciklusmagban minden alkalommal létrehozunk egy új Contact
objektumot, majd beállítjuk a megfelelő field értékeit, melyeket az getXXX
metódussal olvasunk ki a ResultSet
objektumból, ahol XXX
valamilyen alaptípus.
Mivel az SQLite nem tud dátumot tárolni, így azt String
-ként adtuk meg (TEXT
típusú oszlop az adatbázis sémában), így ilyen módon is olvassuk ki.
Ezt egy Date
objektummá alakítjuk majd a toLocalDate()
hívással alakítjuk LocalDate
típusúra.
A következő kódrészlet a hozzáadás/mentés opciót szolgáltatja számunkra.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
|
Az első érdekes dolog, hogy itt egy paraméteres lekérdezésünk lesz, így egy PreparedStatement
-et használunk.
Ugyanakkor, dinamikusan döntjük el, hogy egy meglévő rekord frissítéséről vagy egy új sor beszúrásáról van szó.
Ezt úgy döntjük el, hogy megvizsgáljuk a megkapott kontakt id
-ját, mely egy 0-nál nagyobb egész érték abban az esetben, ha az már létezik és 0, amennyiben az még nem szerepel az adatbázisban.
A következő érdekes momentum, hogy új kontakt létrehozásakor a c.prepareStatement(INSERT_CONTACT, Statement.RETURN_GENERATED_KEYS)
lekérdezést adjuk meg, melyben vegyük észre a RETURN_GENERATED_KEYS
megadást.
Ennek segítségével az executeUpdate
hívás után a ResultSet
-ben elérhetővé válik az összes generált oszlop értéke, azaz például az id
, mely egy generált érték lekérhetővé válik, melyet be is állítunk a mentett kontakt számár, majd ezt az objektumot adjuk vissza.
Az executeUpdate
metódus azt adja vissza, hogy hány sor módosult az SQL utasítás hatására, mely esetünkben 1 kell hogy legyen mind a beszúrás és mind a frissítés esetében is.
Végül lássuk a törlést, melyben sok újdonságot nem látunk, kivéve azt, hogy itt nem használjuk fel az executeUpdate
eredményét.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
Ha megvagyunk a kontaktok kezelésével, akkor érdemes elkészíteni a PhoneDAO
-t is, mely a következőképpen néz ki:
1 2 3 4 5 6 7 |
|
Látható, hogy szeretnénk majd kontakt alapján visszakapni a hozzátartozó telefonszámokat, egy kontakthoz létrehozni egy telefonszámot, illetve törölni egy megadott telefonszámot.
Ezután készítsük el a konkrét implementációt, melyben a findAllByContactId
metódus a következőképpen néz ki:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
|
Alapvetően nincs a megvalósításban semmilyen extra, de nézzük meg, hogy az enum értékét hogyan tároljuk, hiszen ilyet még eddig nem csináltunk.
A PhoneType
attribútumot az adatbázisban egy egész értékként tároljuk (megjegyzem, hogy tárolhatnánk String
-ként is), így amikor azt kinyerjük akkor a PhoneType
enum összes lehetséges értékén (Phone.PhoneType.values()
) végigiterálva (Stream API segítségével), megkeressük azt amelyiknek megegyezik az egész értéke (azaz az ordinal értéke) az adatbázisban megadott számmal.
Amennyiben nem talált ilyet a filterezésünk, úgy az UNKNOWN
enum értéket állítjuk be a létrehozott Phone
objektum PhoneType
attribútumaként.
Az alábbi kódrészlet, melyben a mentést (új rekord vagy rekord update) végezzük szintén nincs túl nagy újdonság, ugyanakkor figyeljünk arra, hogy a PhoneType
-ot a megfelelő egész értékként az ordinal
értékével mentsük!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
|
Végezetül a törlést láthatjuk, mely nagyon hasonló a kontaktnál látottakkal:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
View réteg¶
A megjelenítési réteg jelen esetben egy másik submodule-ban fog helyet kapni, így készítsük is el az előzőeknek megfelelően az új submodule contacts-desktop
néven!
A létrehozáskor a már megismert javafx-maven-archetype
-ot adjuk meg és állítsuk a parent-et szintén a contacts
-ra.
A property-ket a parent pom-ból kapjuk, így a submodule pom.xml
-ben található property-ket kitörölhetjük.
Ezután a próbáljuk ki, hogy a contacts-desktop
modulból elérjük-e a Contact
osztályt!
Elsőre nyilván nem, viszont Alt + Enter esetén fel is kínálja a rendszer, hogy Add dependency on module 'contacts-core'
.
Ekkor a contacts-desktop/pom.xml
-be belekerül a következő:
1 2 3 4 5 6 7 8 |
|
, amely megadja a kapcsolatot a két modul között.
Ezután, ha buildeljük a parent project-et (mvn install
), majd elindítjuk a desktop alkalmazásunkat a javafx:run
plugin goal-al, akkor mindennek megfelelően működnie kell.
Fontos, hogy az mvn install
-t használjuk, hiszen így a legyártott contacts-core
JAR állomány belekerül a lokális Maven repository-ba, ahonnan már tudja használni a contacts-desktop
alkalmazás.
Ezután a contacts-desktop
alkalmazásban a resources
alatt hozzunk létre egy fxml
könyvtárat, melyben az FXML állományainkat fogjuk tárolni!
Hozzunk is létre egy main_window.fxml
állományt ide!
Az FXML állományokhoz controller-eket is fogunk használni, így ezeket a contacts-desktop
-on belül a hu.alkfejl.controller
package-ben fogjuk tárolni.
Hozzuk ide létre a MainWindowController
osztályt, majd adjuk meg ezt a controller-t az imént létrehozott FXML állománynak.
A SceneBuilder segítségével szerkesszük az állományt, melynek eredményeképpen a következő layout-ot kapjuk:
Hasonló eredmény eléréséhez figyeljük meg a bal oldali tree view-t!
Amennyiben ez alapján nem sikerülne összerakni az alkalmazás fő ablakát, akkor segíthet az FXML kód tanulmányozása (melyben már láthatjuk a megadott fx:id
-kat is):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
|
Ezután készítsek el a MainWindowController
állományt és töltsük be az adatbázisból a kontaktok listáját!
Amennyiben nem a javafx-archetype-fxml
-el hoztunk létre a submodule-t, akkor ne felejtsük el hozzáadni a következő függőséget!
1 2 3 4 5 |
|
A fenti függőség szükséges lesz a control elemek injektálásához (@FXML
), illetve az Initializable
interface használatához.
A táblázat értékekkel való feltöltéséhez a következő field injektálásokra lesz szükségünk, illetve magára a ContactDAO
-ra:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
A TableView<Contact>
típusnál a generikus paraméterben azt adhatjuk meg, hogy milyen típusú elemeket tartalmaz a táblázat.
A TableColumn<Contact, String>
megadásoknál a második típusparaméter az aktuális oszlopban megjelenített elem típusa, míg az első mindig megegyezik a TableView
-nak megadott generikus paraméterrel.
Ezután az initialize
metódus és a refreshTable
a következőképpen néz ki:
1 2 3 4 5 6 7 8 9 10 11 |
|
Mivel több helyen is szükség lehet majd a táblázat frissítésére, így ezen műveletet a refreshTable
metódusban adjuk meg.
A táblázathoz hozzárendeltük az adatokat, amik a sorokat adják, viszont a Contact
objektumok fieldjeit le kell képezni a táblázat oszlopaihoz.
Ezt a célt szolgálja a setCellValueFactory
hívás a fenti kódban, mely a cellák értékét adja meg.
Alapvetően a következőképpen lehet használni:
1 2 3 4 5 6 |
|
Itt elevenítsük fel, hogy a Callback
első generikus paramétere a callback metódus paraméterének típusát adja meg (call(CellDataFeatures<Contact, String> c)
), míg a második a visszatérési típusát (ObservableValue<String>
).
A CellDataFeatures
egy wrapper osztály a cellákhoz, hogy minden kapcsolódó adatot elérhessünk az adott TableColumn
megadásnál.
Például a cellához tartozó Contact
objektumot a c.getValue()
hívással kaphatjuk meg, de hozzáférhetünk magához a TableColumn
és a TableView
objektumokhoz is.
A fenti példakódban a nameProperty
-t adjuk vissza, mely lévén egy StringProperty
így ObservableValue<String>
is egyben.
Mivel sokszor van szükség arra, hogy egy bean property-jét használjuk az adott oszlopban, így erre a JavaFX biztosít egy külön megvalósítást, mely a PropertyValueFactory
.
A fenti kódrészlet megfelelője, így a nameColumn.setCellValueFactory(new PropertyValueFactory<>("name"));
sor.
Mielőtt belemennénk az actionColumn
rejtelmeibe, töltsük be az FXML-t az alkalmazás fő belépési pontján.
Mivel több helyen is használhatunk ilyen jellegű betöltést, így ezt egy statikus metódusba szervezzük ki, mely az FXML állomány URL-jét várja.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
|
A start
metódusban elmentjük a kapott stage
-et, majd meghívjuk a loadFXML
metódust.
Fontos, hogy visszaadjuk az FXMLLoader objektumot, amit jelen esetben nem használunk ugyan fel, de később még jól jöhet olyan esetben, amikor szükségünk lehet a controller-re.
Ezen a ponton próbáljuk ki, hogy megjelenik-e a táblázatban a megfelelő adat!
Ne felejtsünk el az adatbázishoz hozzáadni adatokat, melyet az IDE-n belül könnyen megtehetünk, ha a CONTACT
táblára kattintunk a Database alatt!
A megjelenő adatok felett a + jelre kattintva egy új sort adhatunk a táblához.
Miután megadtuk a szükséges adatokat, ne felejtsük el commit-álni a változtatásokat a gomb megnyomásával.
Az adatbázis mellett ellenőrizzük azt is, hogy az FXML-ben az fx:id
megadások szerepelnek-e!
Ezután adjuk meg a műveleteket megjelenítő oszlopot is (actionsColumn
)!
Elsőként injektáljuk ezt a controllerbe, illetve adjuk meg a megfelelő helyen az FXML-ben is az fx:id
-t!
1 2 |
|
Az első dolog, hogy a TableColumn
második generikus paraméterében a Void
típust adjuk meg, hiszen ez az oszlop nem egy konkrét property-hez kapcsolódik, így az érték típusát sem tudjuk megadni, ezért ez Void
lesz.
Ennek következményeképpen nem is adunk meg cellValueFactory
-t, hiszen a cella értéke Void
.
Ugyanakkor ez számunkra nem is probléma, mert csak gombokat szeretnénk elhelyezni ebben az oszlopban.
Azt, hogy az adott cellában mi jelenjen meg (a renderelés szempontjából megközelítve), azt a cellFactory
határozza meg.
Ennek függvényében az initialize(...)
metódust a következőképpen egészítsük ki:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
|
A setCellFactory
paraméterében egy Callback <TableColumn<S,T>,TableCell<S,T>> value
paramétert vár, melyet lambda kifejezés formájában meg is adunk.
Ebből ugye látszik, hogy egy TableCell<S,T>
típusú elemet kell visszaadnunk, melyet meg is adunk úgy, hogy közben a TableCell
osztályt kiterjesztjük (hasonlóan az anonymous inner class-okhoz, de itt osztályra alkalmazzunk, interface helyett).
A kiterjesztésben két Button
field-et adunk hozzá, illetve az init blokkban (mivel a leszármaztatott osztálynak nincs neve itt) megadjuk ezen gombok működését is.
Egy TableCell
objektum képes lekérni a sort amihez tartozik (getTableRow
), mely sorhoz tartozó elemet a getItem
-el kérhetünk le, amelynek típusa megegyezik a TableCell<S,T>
S
paraméterével, jelen esetben a Contact
-al.
A deleteContact
és editContact
metódusokat azonnal látjuk, de előtte fókuszáljunk az updateItem
metódus felülírására.
Az updateItem
-et soha ne hívjuk meg manuálisan (annak hívása a grafikus elem, jelen dolga), ugyanakkor, ha egy cella kinézetét szeretnénk személyre szabni, akkor ahhoz ezt a metódust a legcélszerűbb felüldefiniálni.
Itt két szabály van:
- Mindig hívjuk meg a
super.updateItem(item, empty);
metódusát - Mindig csekkoljuk az üres cellákat (előfordulhat, hogy egy sorban nincs semmilyen megjelenítendő elem) és ott állítsuk
null
-ra a hozzátartozó grafikát, már ha ilyen esetben tényleg nem akarunk semmit látni a cellában
A fentiek után egy HBox
-ra egyszerűen elhelyezzük a két gombot, majd ezt a konténert rajzoltatjuk ki.
Miután a fentieket megértettük, lássuk a deleteContact
metódust:
1 2 3 4 5 6 7 8 |
|
Mielőtt egyből kitörölnénk az adott kontaktot, megerősítést várunk a felhasználótól, melyet az Alert
osztály használatával tudjuk megvalósítani.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
A következő gyakorlaton innen folytatjuk majd.