Kihagyás

6. gyakorlat

Contact alkalmazás folytatása

Az előző óra végén a listából a kontaktok törlését megvalósítottuk. Mielőtt a módosítást megcsinálnánk elkészítjük a fő ablak menüjében a Close menüpont eseménykezelését.

1
2
3
4
5
6
7
8
public class MainWindowController {
    ...

    @FXML
    public void onExit(){
        Platform.exit();
    }
}

Az FXML-ben a következőnek kell szerepelnie, melyet a SceneBuilder-rel is megadhatunk (Code -> onAction):

1
<MenuItem mnemonicParsing="false" text="Close" onAction="#onExit"/>

Ezzel meg is vagyunk, így nézzük az editContact metódust! Mivel a szerkesztéshez egy új ablakra lesz szükségünk, ahol magát a kontaktot szerkeszteni tudjuk, így létre is fogunk hozni egyet /fxml/add_edit_contact.fxml néven a resources mappa alatt és ezt az FXML állományt fogjuk betölteni. Ezt a formot majd a hozzáadásnál is használhatjuk, így ilyen szemlélettel alakítjuk ki a felületet, illetve a neve is ezt tükrözi.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
private void editContact(Contact c) {
    FXMLLoader fxmlLoader = App.loadFXML(("/fxml/add_edit_contact.fxml"));
    AddEditContactController controller = fxmlLoader.getController();
    controller.setContact(c);
}

@FXML
public void onAddNewContact(){ // kössük be az Edit/Add alá
    FXMLLoader fxmlLoader = App.loadFXML(("/fxml/add_edit_contact.fxml"));
    AddEditContactController controller = fxmlLoader.getController();
    controller.setContact(new Contact()); // ennyi a különbség
}

Mivel a form-on szeretnénk majd megjeleníteni a szerkeszteni kívánt kontakt információit, így ezt át fogjuk adni a controller számára. Hozzuk is létre az FXML állományt és a hozzá tartozó AddEditContactController-t! Készítsük el a felületet mely a következőképpen néz ki a SceneBuilder-ben: A bal oldalon látható TreeView segítséget nyújt a pontos elrendezés kialakításában

Add/Edit Contact

Az elemek pontos tulajdonságánál segítséget nyújt az FXML kód, melyből láthatjuk a megadott fx:id-kat és az eseménykezelőket 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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.text.*?>

<GridPane prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/11.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="hu.alkfejl.controller.AddEditContactController">
   <rowConstraints>
      <RowConstraints minHeight="10.0" percentHeight="8.0" prefHeight="30.0" />
      <RowConstraints minHeight="10.0" percentHeight="8.0" prefHeight="30.0" />
      <RowConstraints minHeight="10.0" percentHeight="8.0" prefHeight="30.0" />
      <RowConstraints maxHeight="60.0" minHeight="10.0" percentHeight="36.0" prefHeight="33.0" />
      <RowConstraints maxHeight="28.0" minHeight="0.0" percentHeight="8.0" prefHeight="27.0" />
      <RowConstraints minHeight="10.0" percentHeight="8.0" prefHeight="30.0" />
      <RowConstraints minHeight="10.0" percentHeight="8.0" prefHeight="30.0" />
      <RowConstraints minHeight="10.0" percentHeight="8.0" prefHeight="30.0" />
      <RowConstraints minHeight="10.0" percentHeight="8.0" prefHeight="30.0" />
   </rowConstraints>
   <columnConstraints>
      <ColumnConstraints minWidth="10.0" percentWidth="30.0" prefWidth="100.0" />
      <ColumnConstraints minWidth="10.0" percentWidth="70.0" prefWidth="100.0" />
      <ColumnConstraints minWidth="10.0" percentWidth="70.0" prefWidth="100.0" />
   </columnConstraints>
   <children>
      <StackPane prefHeight="150.0" prefWidth="200.0" GridPane.columnSpan="2">
         <children>
            <Label text="Add or Edit Contact">
               <font>
                  <Font size="20.0" />
               </font>
            </Label>
         </children>
      </StackPane>
      <Label text="Name" GridPane.rowIndex="1" />
      <Label text="Email" GridPane.rowIndex="2" />
      <Label text="Phone Numbers" GridPane.rowIndex="3" />
      <Label text="Address" GridPane.rowIndex="4" />
      <Label text="Date of Birth" GridPane.rowIndex="5" />
      <Label text="Company" GridPane.rowIndex="6" />
      <Label text="Position" GridPane.rowIndex="7" />
      <HBox alignment="CENTER" prefHeight="100.0" prefWidth="200.0" spacing="10.0" GridPane.columnSpan="2" GridPane.rowIndex="8">
         <children>
            <Button fx:id="saveBtn" mnemonicParsing="false" onAction="#onSave" text="Save" />
            <Button mnemonicParsing="false" onAction="#onCancel" text="Cancel" />
         </children>
         <padding>
            <Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
         </padding>
      </HBox>
      <TextField fx:id="name" GridPane.columnIndex="1" GridPane.rowIndex="1" />
      <TextField fx:id="email" GridPane.columnIndex="1" GridPane.rowIndex="2" />
      <TextField fx:id="address" GridPane.columnIndex="1" GridPane.rowIndex="4" />
      <TextField fx:id="company" GridPane.columnIndex="1" GridPane.rowIndex="6" />
      <TextField fx:id="position" GridPane.columnIndex="1" GridPane.rowIndex="7" />
      <DatePicker fx:id="dateOfBirth" prefHeight="25.0" prefWidth="421.0" GridPane.columnIndex="1" GridPane.rowIndex="5" />
      <VBox prefHeight="200.0" prefWidth="100.0" spacing="10.0" GridPane.columnIndex="1" GridPane.rowIndex="3">
         <children>
            <ListView fx:id="phones" prefHeight="200.0" prefWidth="200.0" />
            <Button mnemonicParsing="false" onAction="#addNewPhone" text="Add">
               <VBox.margin>
                  <Insets bottom="5.0" />
               </VBox.margin>
            </Button>
         </children>
      </VBox>
      <Label fx:id="nameErrors" textFill="RED" GridPane.columnIndex="2" GridPane.rowIndex="1" />
      <Label fx:id="emailErrors" textFill="RED" GridPane.columnIndex="2" GridPane.rowIndex="2" />
   </children>
</GridPane>

Ezen a ponton ellenőrizzük, hogy az edit contact gomb megnyomása esetén sikerül-e betölteni az FXML állományt, de ehhez először szükségünk van a setContact metódus definíciójára.

1
2
3
4
5
6
7
8
public class AddEditContactController {

    private Contact contact;

    public void setContact(Contact c) {
        this.contact = c;
    }
}

Fejlesszük tovább az AddEditContactController-t úgy, hogy az FXML-ben megadott vezérlőket injektáljuk a controller-be:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public class AddEditContactController {
    ...
    @FXML
    private Button saveBtn;

    @FXML
    private TextField name;
    @FXML
    private TextField email;

    @FXML
    ListView<Phone> phones;

    @FXML
    private TextField address;
    @FXML
    private DatePicker dateOfBirth;
    @FXML
    private TextField company;
    @FXML
    private TextField position;

Ezután a felületi vezérlők értékét (a fent injektált elemek) be szeretném állítani a megkapott contact megfelelő property értékeire. Így itt kötéseket fogok alkalmazni, melyet akkor hozok létre, amikor a setContact-ot meghívja valaki (azaz új kontakt kerül a felülethez). A setContact ennek megfelelően a következőképpen néz ki:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public void setContact(Contact c) {
    this.contact = c;

    List<Phone> phonesList = phoneDAO.findAllByContactId(c.getId());
    contact.setPhones(FXCollections.observableArrayList(phonesList));

    name.textProperty().bindBidirectional(contact.nameProperty());
    email.textProperty().bindBidirectional(contact.emailProperty());
    phones.itemsProperty().bindBidirectional(contact.phonesProperty());
    address.textProperty().bindBidirectional(contact.addressProperty());
    dateOfBirth.valueProperty().bindBidirectional(contact.dateOfBirthProperty());
    company.textProperty().bindBidirectional(contact.companyProperty());
    position.textProperty().bindBidirectional(contact.positionProperty());

}

Mivel a contact alapvetően nem tud a hozzá tartozó phone-okról, így ezt a phoneDAO-tól kérjük le, melyet field-ként adjunk is meg a controller-ben:

1
private PhoneDAO phoneDAO = new PhoneDAOImpl();

A ListView minket érdeklő property-je az itemsProperty, mely a benne lévő elemeket adja meg ObjectProperty <ObservableList <T>> formájában, ahol a T típusparaméter jelen esetben nyilván Phone. A Contact bean-ben nem véletlenül ilyen módon adjuk meg a telefonokat (ObjectProperty<ObservableList<Phone>>). A dateOfBirth vezérlő egy valueProperty-vel rendelkezik, mely visszaad egy ObjectProperty<T> property-t, ahol T jelenleg LocalDate, melyet szintén ilyen módon adtunk meg a Contact osztályban.

A kétirányú kötés inicializálása fontos, mégpedig olyan szempontból, hogy az értékeket szinkronba fogjuk hozni és így az egyik property elveszti a jelenlegi értékét. Ha vesszük például a name property kötését, akkor a name felületi vezérlő értékét kötjük hozzá a contact nameProperty-jéhez, azaz a contact értéke másolódik a name felületi vezérlő property-jébe. Fordított esetben, azaz: contact.nameProperty().bindBidirectional(name.textProperty()); esetében a contact objektumunk elvesztette volna az aktuális értékét és a felületen sem jelenne meg semmilyen érték.

Ezen a ponton ellenőrizzük, hogy a megadott elemek megjelennek-e a felületen (Az adatbázisban adjunk meg ).

A telefonok listájában, azaz a ListView<Phone> elemben az egy cellában megjelenő elemekre alapból a toString()-et hívja a rendszer és ezt írja ki a cellába, ezért láthatunk rosszul megjelenő (Object osztály toString() alapú) adatokat. Nincs más teendő, mint a Phone osztály toString metódusát elkészíteni:

1
2
3
4
5
6
7
8
public class Phone {
    ...

    @Override
    public String toString() {
        return number.getValue() + " ("+ phoneType.getValue() + ")";
    }
}

Ha ezekkel megvagyunk akkor jöhet a tényleges mentés, de előtte implementáljuk a Cancel gomb eseménykezelését, mely szimplán annyit tesz, hogy visszatölti a fő ablakot.

1
2
3
4
@FXML
public void onCancel(){
    App.loadFXML("/fxml/main_window.fxml");
}

A mentéshez szükségünk lesz egy ContactDAO-ra, így a következő field megadást is használjuk:

1
private ContactDAO contactDAO = new ContactDAOImpl();

Maga a mentés a következőképpen néz ki:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@FXML
public void onSave(){
    contact = contactDAO.save(contact);
    phoneDAO.deleteAll(contact.getId());
    contact.getPhones().forEach(phone -> {
        phone.setId(0);
        phoneDAO.save(phone, contact.getId());
    });
    App.loadFXML("/fxml/main_window.fxml");
}

Elsőre furán nézhet ki a telefonszámok törlése, majd létrehozása. Ezt azért csináljuk ilyen módon, mert majd a telefonszámokat is módosítani szeretnénk, ugyanakkor azokat egyből nem szeretnénk menteni, csak akkor amikor a kontakt szerkesztés/hozzáadás felületen megnyomjuk a Save gombot. Mivel kicsit nehezebb így számontartani, hogy mi változott, a legegyszerűbb az, ha minden számot kitörlünk és újra hozzáadjuk őket (lehet, hogy nem is változott semmi, de ettől még működni fog a fenti kódrészlet csak kis plusz melót végez).

A deleteAll phoneDAO metódus még nem létezik, de azt nagyon könnyen implementálhatjuk (nyilván adjuk hozzá az interface-hez is a metódus fejlécét):

1
2
3
4
@Override
public void deleteAll(int contactId) {
    findAllByContactId(contactId).forEach(this::delete);
}

Miután elmentettük a contact-ot és a telefonszámokat, ebben az esetben is visszatérünk a fő ablakhoz.

A telefonok listájánál végigvihetjük a kontakt táblázatban látott megvalósítást is, amikor az Edit és a Delete gombokat hozzáadtuk egy külön oszlopban. A ListView-nál ez annyiban különbözne, hogy valamilyen Label-t is hozzáadnánk, melyen magát a telefonszámot adjuk meg, továbbá a két gombot is. Ez azért kell mert a ListView csak elemek sora, azaz olyan mintha egy egy oszlopos TableView-nk lenne. Annak érdekében, hogy egy újabb lehetőséget ismerjünk meg, így egy ContextMenu fogunk készíteni (jobb klikk az adott ListView elemen), melyben az Edit és a Delete menüpontokat fogjuk elhelyezni. Ehhez szükségünk lesz az initialize metódusra, így a controllert implementálja az Initializable interface-t! Maga az initialize a következőképpen alakul:

 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
@Override
public void initialize(URL location, ResourceBundle resources) {
    phones.setCellFactory(param -> {
        ListCell<Phone> cell = new ListCell<>();
        ContextMenu contextMenu = new ContextMenu();

        MenuItem editItem = new MenuItem("Edit");
        MenuItem deleteItem = new MenuItem("Delete");

        contextMenu.getItems().addAll(editItem, deleteItem);

        editItem.setOnAction(event -> {
            ...
        });
        deleteItem.setOnAction(event -> {
            ...
        });

        StringBinding cellTextBinding = new When(cell.itemProperty().isNotNull()).then(cell.itemProperty().asString()).otherwise("");
        cell.textProperty().bind(cellTextBinding);

        cell.emptyProperty().addListener((observable, wasEmpty, isNowEmpty) -> {
            if(isNowEmpty){
                cell.setContextMenu(null);
            } else{
                cell.setContextMenu(contextMenu);
            }

        });
        return cell;

    });
}

A setCellFactory​(Callback <ListView<T>,​ListCell<T>> value)-ban megadott CAllback egy ListCell<T> ad vissza. Ezt a cellát létre is hozzuk és később fel is használjuk, előtte azonban létrehozzuk a ContextMenu-t és hozzáadjuk a két MenuItem objektumot, amelyeket szintén itt hoztunk létre. A gombok viselkedését később nézzük meg, most elég a többi részt értelmeznünk, ahhoz, hogy a context menüt meg tudjuk jeleníteni. Mivel egy ListView tartalmazhat több sort, mint amennyi tényleges Phone van benne így kezelnünk kell azt az esetet, ha az adott sor üres. A cella szövegét ettől tesszük függővé a new When(cell.itemProperty().isNotNull()).then(cell.itemProperty().asString()).otherwise(""); kötés létrehozásával, mely kötés aktuális értékét hozzákötjük a cella textProperty-kéhez.

Ezen felül csinálunk egy ChangeListener-t is, mely azt figyeli, hogy ha az adott cella értéke üres lesz akkor nem ad hozzá context menüt (cell.setContextMenu(null);), illetve ha van érték a sorban, akkor hozzáadja a context menüt.

Végül visszaadjuk magát a cell-t.

Ezután nézzük az eseménykezelőket! A legfontosabb, hogy tudnunk kell, hogy melyik soron kattintottunk a kontextus menü elemekre (melyik Phone objektumot szeretnénk módosítani vagy törölni). Ehhez a cell.getItem() metódus ad segítséget, mely visszaadja a cellához rendelt Phone objektumot. Kezdjük a törléssel, mert az egyszerűbb:

1
2
3
deleteItem.setOnAction(event -> {
    contact.getPhones().remove(cell.getItem());
});

Mivel a contact mentésénél törlünk minden az adott contact-hoz rendelt Phone-t, majd utána a következőképpen mentjük a telefonszámokat: contact.getPhones().forEach(...), így elegendő, ha a contact.getPhones() listából kitöröljük azt a Phone-t, amelynek cellájában a context menü törlést kiválasztottuk. Fontos látni, hogy ezen a ponton az adatbázisban semmit sem matattunk.

A módosításhoz szükség lesz egy form-ra, melyet egy modális ablakban (nem lehet belőle kikattintani) szeretnénk megjeleníteni. Ezért itt hívjuk meg a showPhoneDialog metódust az adott Phone típusú paraméterrel!

1
2
3
4
editItem.setOnAction(event -> {
    Phone item = cell.getItem();
    showPhoneDialog(item);
});

Maga a showPhoneDialog 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
private void showPhoneDialog(Phone phone) {
    Stage stage = new Stage();
    stage.initModality(Modality.APPLICATION_MODAL);

    FXMLLoader loader = new FXMLLoader(getClass().getResource("/fxml/add_edit_phone.fxml"));

    try {
        Parent root = loader.load();
        AddEditPhoneController controller = loader.getController();
        controller.init(stage, phone, contact);
        stage.setScene(new Scene(root));
        stage.showAndWait();

    } catch (IOException e) {
        e.printStackTrace();
    }
}

A kód nagy része megegyezik azzal amit az App.loadFXML-en belül megcsináltunk. Itt egy saját Stage-be szeretném belerakni a tartalmat, így a fenti megközelítést alkalmazom. Ami fontos, hogy a kontrolleren beállítjuk a stage, phone és contact objektumokat, mivel ezekre szükség lesz (később látjuk).

Challenge

Oldjuk meg úgy, hogy az eddigi hívásokat ne kelljen átírnunk, viszont legyen lehetőség arra is, hogy a fenti problémát kiküszöböljük. Az egyik probléma az, hogy van olyan eset, amikor műveleteket is szeretnénk elvégezni a controller-en, a másik egyszerűbb probléma, hogy mi legyen a Stage-el.

Megoldás

Az App.java a következőképpen változik:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public static FXMLLoader loadFXML(String fxml){
    return loadFXML(fxml, stage, o -> {});
}

public static <T> FXMLLoader loadFXML(String fxml, Stage stage, Consumer<T> controllerOps){
    FXMLLoader loader = new FXMLLoader(App.class.getResource(fxml));
    Scene scene = null;
    try {
        Parent root = loader.load();
        controllerOps.accept(loader.getController());
        scene = new Scene(root);
        stage.setScene(scene);
    } catch (IOException e) {
        e.printStackTrace();
    }

    return loader;

}

Ilyen módon sehol nem kell változtatnunk a meglévő loadFXML hívásokon, azonban a showPhoneDialog-on belül tudjuk a következőt csinálni:

1
2
3
4
5
6
Stage stage = new Stage();
stage.initModality(Modality.APPLICATION_MODAL);
App.<AddEditPhoneController>loadFXML("/fxml/add_edit_phone.fxml", stage, (controller) -> {
    controller.init(stage, phone, contact);
});
stage.showAndWait();

Kössük be az Add gombhoz is a showPhoneDialog-ot (onAction legyen beállítva a gombon):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
...
@FXML
public void addNewPhone(){
    showPhoneDialog();
}

private void showPhoneDialog(){
    showPhoneDialog(new Phone());
}
...

Ezután hozzuk létre az /fxml/add_edit_phone.fxml állományt, illetve a hozzá tartozó AddEditPhoneController-t. Hozzuk is létre a controller-ben az init metódust:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public class AddEditPhoneController implements Initializable {

    private Stage stage;
    private Phone phone;
    private Contact contact;

    public void init(Stage stage, Phone phone, Contact contact) {
        this.stage = stage;
        this.phone = phone;
        this.contact = contact;
    }
}

Miután ezzel megvagyunk, alakítsuk ki a következő felületet a SceneBuilder segítségével (itt is segít a bal oldali tree view):

Add/Edit Phone

Az FXML-hez tartozó, generált FXML kód a követkető:

 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
<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<GridPane prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/11.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="hu.alkfejl.controller.AddEditPhoneController">
   <rowConstraints>
      <RowConstraints minHeight="10.0" percentHeight="10.0" prefHeight="30.0" />
      <RowConstraints minHeight="10.0" percentHeight="10.0" prefHeight="30.0" />
      <RowConstraints minHeight="10.0" percentHeight="10.0" prefHeight="30.0" />
   </rowConstraints>
   <columnConstraints>
      <ColumnConstraints minWidth="10.0" percentWidth="50.0" prefWidth="100.0" />
      <ColumnConstraints minWidth="10.0" percentWidth="50.0" prefWidth="100.0" />
   </columnConstraints>
   <children>
      <Label text="Number" />
      <Label text="PhoneType" GridPane.rowIndex="1" />
      <HBox alignment="CENTER" prefHeight="100.0" prefWidth="200.0" spacing="10.0" GridPane.columnSpan="2" GridPane.rowIndex="2">
         <children>
            <Button mnemonicParsing="false" onAction="#onSave" text="Save" />
            <Button mnemonicParsing="false" onAction="#onCancel" text="Cancel" />
         </children>
      </HBox>
      <TextField fx:id="number" GridPane.columnIndex="1" />
      <ComboBox fx:id="phoneType" prefHeight="25.0" prefWidth="298.0" GridPane.columnIndex="1" GridPane.rowIndex="1" />
   </children>
   <padding>
      <Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
   </padding>
</GridPane>

Az egyetlen új vezérlő a ComboBox, melyet a PhoneType-nál alkalmazunk. Ezen a ponton ellenőrizhetjük, hogy megfelelően betölti-e az oldalt az alkalmazás (mind az Add és mind az Edit esetén).

Ezután a controllerbe injektáljuk a szükséges grafikus vezérlő elemeket:

1
2
3
4
5
@FXML
private TextField number;

@FXML
private ComboBox<Phone.PhoneType> phoneType;

A ComboBox választható elemeinek a megadásához használjuk az Initializable interface initialize metódusát:

1
phoneType.getItems().setAll(Phone.PhoneType.values());

Itt lekérjük az összes lehetséges enum értéket és ezt állítjuk be lenyíló doboznak.

A korábban megírt init metódusban ezután a kapott Phone tulajdonságok alapján hozzuk létre a felületi kötéseket, hasonlóan mint ahogy azt a Contact esetében is megtettük:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public void init(Stage stage, Phone phone, Contact contact) {
    this.stage = stage;
    this.phone = phone;
    this.contact = contact;

    number.textProperty().bindBidirectional(this.phone.numberProperty());
    phoneType.valueProperty().bindBidirectional(this.phone.phoneTypeProperty());

    if(phoneType.getSelectionModel().isEmpty()){
        phoneType.getSelectionModel().selectFirst(); 
    }
}

A kötések létrehozása után megvizsgáljuk, hogy az adott Phone-hoz tartozik-e phoneType (kötés miatt ezt látjuk már), amennyiben nem akkor egy új telefonról van szó és ilyen esetben az első lehetséges opciót választjuk ki az enum értékei közül.

A felületen elhelyezett Cancel gomb hatására az init-ben beállított stage-et fogom bezárni:

1
2
3
4
@FXML
public void onCancel(){
    stage.close();
}

A Save egy kicsit fura felépítésű lesz:

1
2
3
4
5
6
@FXML
public void onSave(){
    contact.getPhones().remove(phone);
    contact.getPhones().add(phone);
    stage.close();
}

A törlés és utána hozzáadás ugyanarra az objektumra redundánsnak tűnik, de mivel ez felelős a hozzáadás és a módosításért is, így van létjogosultsága (új elem esetén nyilván nem tudja kitörölni, csak hozzáadni).

Ellenőrzések és kiegészítések

Az első egy apró kiegészítés a kontakt törléshez, mivel a kontakthoz rendelt telefonszámokat nem töröltük ki. A megerősítés után ezt megtehetjük a következőképpen:

1
2
3
4
5
6
7
8
9
private void deleteContact(Contact c) {
    Alert confirm = new Alert(Alert.AlertType.CONFIRMATION, "Are you sure you want to delete contact: " + c.getName(), ButtonType.YES, ButtonType.NO);
    confirm.showAndWait().ifPresent(buttonType -> {
        if(buttonType.equals(ButtonType.YES)){
            phoneDAO.deleteAll(c.getId());  // hozzuk létre a phoneDAO fieldet is!
            dao.delete(c);
        }
    });
}

Contact ellenőrzések

Ezután végezzünk el a kötelező elemek megadásának ellenőrzését az AddEditContactController-ben: Először is le szeretnénk tiltani a gombot (nem lehet megnyomni), addig amíg van érvénytelen adat. Helyezzük el a két Label-t a felületen, ahova a hibaüzeneteket fogjuk írni, illetve injektáljuk is ezeket a controllerbe:

1
2
3
4
5
6
7
8
@FXML
private Button saveBtn;

@FXML
private Label nameErrors;

@FXML
private Label emailErrors;

Az initialize metódust ezután a következőképpen módosítjuk:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
saveBtn.disableProperty().bind(name.textProperty().isEmpty()
        .or(email.textProperty().isEmpty()
                .or(dateOfBirth.valueProperty().isNull())));

name.textProperty().addListener((observable, oldValue, newValue) -> {
    if(newValue != null && newValue.isEmpty()){
        nameErrors.setText("Name is required");
    }
    else{
        nameErrors.setText("");
    }
});

email.textProperty().addListener((observable, oldValue, newValue) -> {
    if(newValue != null && newValue.isEmpty()){
        emailErrors.setText("Email is required");
    } else{
        emailErrors.setText("");
    }
});      

Így, a gomb egészen addig disabled állapotban lesz, ameddig vagy a name vagy az email vagy a születési dátum üres/null. Az egyes hibajelző Label-ek értékét ChangeListener-ek alapján állítjuk be.

Keresés a kontaktok listájában

Először alakítsuk ki a felületi vezérlőket a main_window.fxml oldalon. A BorderPane bal oldali eleméhez adjuk hozzá a következőket:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<BorderPane prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/11.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="hu.alkfejl.controller.MainWindowController">
   ...
   <left>
      <VBox spacing="10.0" prefHeight="200.0" prefWidth="100.0" BorderPane.alignment="CENTER">
         <children>
            <Label text="Search by Name" />
            <TextField fx:id="nameSearch" onKeyReleased="#onSearch" />
            <Label text="Search by Email" />
            <TextField fx:id="emailSearch" onKeyReleased="#onSearch" />
         </children></VBox>
   </left>
</BorderPane>

A MainWindowController-en belül injektáljuk a két TextField objektumot, majd készítsük el az onSearch metódust. Jelen esetben nem adatázisműveletként valósítjuk meg a keresést (bár valós környezetben így volna szép), hanem a lekért teljes listát filterezzük. Emiatt a refreshTable-t módosítjuk úgy, hogy a kontaktok listáját egy field-ben áltároljuk.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
private List<Contact> all;

@FXML
private TextField nameSearch;

@FXML
private TextField emailSearch;

private void refreshTable() {
    all = dao.findAll();
    contactTable.getItems().setAll(all);
}

@FXML
public void onSearch(){
    List<Contact> filtered = all.stream().filter(contact -> contact.getName().contains(nameSearch.getText()) && contact.getEmail().contains(emailSearch.getText())).collect(Collectors.toList());
    contactTable.getItems().setAll(filtered);
}

Az onSearch-ön belül Stream API-t használunk az elemek szűréséhez, majd ezt állítjuk be a táblázat elemeiként.

Videók

Gyakorló Projekt - Pizzázás

A feladat egy pizzanyilvántartó program elkészítése Java nyelven, amely követi az MVC modellt. A programnak a következő funkcionalitásokat kell támogatnia.

Pizza felvétele

A programnak támogatnia kell a pizza felvételét, ahol a következő adatokat kell felvennie:

  • Pizza neve, ami szövegesen adható meg
  • Pizza leírása, ami tartalmazza, hogy milyen összetevői vannak a pizzának
  • Vegetáriánus jelző (Checkbox)
  • És végül az ára, ami egy egész szám (Spinner, Slider)

Megszorítások, követelmények:

  • A pizza beszúrásakor kapjon egy egyedi azonosítót.
  • A sikeres beszúráskor egy ablak jelenjen meg, amely megmondja a beszúrt pizza sorszámát.
  • A rendszer nem tárolhat két ugyanolyan nevű pizzát. Ha ugyanolyan nevű pizzát akarunk beszúrni, akkor a rendszer adjon hibaüzenetet (de ne zárja be a beviteli ablakot).

AF-pizza-felvetel

AF-pizza-felvetel-sb

Pizzák listázása

A programnak támogatnia kell a pizzák listázását. Nem kell, hogy automatikusan történjen, elegendő valamilyen funkcionalitást biztosítani erre.

AF-pizza-lista

Pizza rendelés

A programnak támogatni kell a pizza rendelést is. Ez azt jelenti, hogy a felhasználó kiválaszthatja, hogy milyen pizzát szeretne rendelni, mekkora méretűt, hány darabot, és akkor megrendelheti.

  • A pizzát kiválasztani a legördülő menü segítségével lehet, ahol az összes elérhető pizza közül lehet választani.
    • A kiválasztott pizza tulajdonságai jelenjen meg alatta.
  • A kicsi pizza 20%-kal olcsóbb, mint a normál méretű, míg a nagy 20%-kal drágább.

AF-pizza-rendeles

Típushibák

Fordítási hibák

  • Nem implementált örökölt metódus
  • Absztrakt osztály példányosítása
  • FXML
    • Rossz osztály van megadva
    • @FXML annotáció hiánya
    • Rossz az FXML fájl útvonala
  • Rossz import: javafx.valami helyett például java.awt.valami
  • Örökölt metódus láthatósága csökken

Futási hibák

Adatbázis, DAO

  • Rossz az SQL parancs, ami létrehozza a kapcsolatot az adatbázis felé
  • getConnection("jdbc:sqlite:" + DBFILE); helyett
    • getConnection("jdbc:sqlite" + DBFILE);
    • getConnection("jdbc:sqlite: + DBFILE");
    • getConnection("jdbc.sqlite:" + DBFILE);
  • INSERT INTO Pizza (Nev, Leiras, Vega, Ar) VALUES (?, ?, ?, ?) hibás
    • Hibás táblanév, mezővés
    • VALUES helyett VALUE
  • PreparedStatement esetében a setXXX esetében az indexelésnek 1-től kell indulnia, és nem 0-tól
  • A DAO-ban Adatbázis fájl rosszul van megadva

View

  • Rosszul összerakott GUI
  • Egy elem kétszer is előfordul a fában

Logikai hibák

Olyan hibák, amelyek nem vezetnek futási hibához, de nem jól működik a program

  • A DAO vagy Controller esetében az interfésznek kell lennie a statikus típusnak, és a konkrét megvalósítás a dinamikus típus
  • DAO-ban a list esetében nem töröljük a listát, mielőtt ismét feltöltenénk
  • ConstructTable többszöri meghívása (plusz oszlopok lesznek minden hívásnál)
  • Rossz képletek, inicializálatlan értékek, objektumok

Utolsó frissítés: 2021-03-19 09:17:27