位置:首頁 > Java技術 > JavaFX教學 > JavaFX - XML格式存儲

JavaFX - XML格式存儲

Screenshot AddressApp Part 5

第5部分的主題

  • 持久化數據為XML
  • 使用JavaFX的FileChooser
  • 使用JavaFX的菜單
  • 用戶設置中保存最後打開的文件路徑。

現在我們的地址應用程序的數據隻保存在內存中。每次我們關閉應用程序,數據將丟失,因此是時候開始考慮持久化存儲數據了。

保存用戶設置

Java允許我們使用Preferences類保存一些應用狀態。依賴於操作係統,Perferences保存在不同的地方(例如:Windows中的注冊文件)。

我們不能使用Preferences來保存全部地址簿。但是它允許我們保存一些簡單的應用狀態。一件這樣事情是最後打開文件的路徑。使用這個信息,我們能加載最後應用的狀態,不管用戶什麼時候重啟應用程序。

下麵兩個方法用於保存和檢索Preference。添加它們到你的MainApp類的最後:

MainApp.java
/**
 * Returns the person file preference, i.e. the file that was last opened.
 * The preference is read from the OS specific registry. If no such
 * preference can be found, null is returned.
 * 
 * @return
 */
public File getPersonFilePath() {
    Preferences prefs = Preferences.userNodeForPackage(MainApp.class);
    String filePath = prefs.get("filePath", null);
    if (filePath != null) {
        return new File(filePath);
    } else {
        return null;
    }
}

/**
 * Sets the file path of the currently loaded file. The path is persisted in
 * the OS specific registry.
 * 
 * @param file the file or null to remove the path
 */
public void setPersonFilePath(File file) {
    Preferences prefs = Preferences.userNodeForPackage(MainApp.class);
    if (file != null) {
        prefs.put("filePath", file.getPath());

        // Update the stage title.
        primaryStage.setTitle("AddressApp - " + file.getName());
    } else {
        prefs.remove("filePath");

        // Update the stage title.
        primaryStage.setTitle("AddressApp");
    }
}

持久性數據到XML

為什麼是XML?

持久性數據的一種最常用的方法是使用數據庫。數據庫通常包含一些類型的關係數據(例如:表),當我們需要保存的數據是對象時。這稱object-relational impedance mismatch。匹配對象到關係型數據庫表有很多工作要做。這裡有一些框架幫助我們匹配(例如:Hibernate,最流行的一個)。但是它仍然需要相當多的設置工作。

對於簡單的數據模型,非常容易使用XML。我們使用稱為JAXB(Java Architecture for XML Binding)的庫。隻需要幾行代碼,JAXB將允許我們生成XML輸出,如下所示:

示例XML輸出
<persons>
    <person>
        <birthday>1999-02-21</birthday>
        <city>some city</city>
        <firstName>Hans</firstName>
        <lastName>Muster</lastName>
        <postalCode>1234</postalCode>
        <street>some street</street>
    </person>
    <person>
        <birthday>1999-02-21</birthday>
        <city>some city</city>
        <firstName>Anna</firstName>
        <lastName>Best</lastName>
        <postalCode>1234</postalCode>
        <street>some street</street>
    </person>
</persons>

使用JAXB

JAXB已經包含在JDK中。這意味著我們不需要包含任何其它的庫。

JAXB提供兩個主要特征:編列(marshal)Java對象到XML的能力,反編列(unmarshal)XML到Java對象。

為了讓JAXB能夠做轉換,我們需要準備我們的模型。

準備JAXB的模型類

我們希望保持的數據位於MainApp類的personData變量中。JAXB要求使用@XmlRootElement注釋作為最頂層的類。personDataObservableList類,我們不能把任何注釋放到ObservableList上。因此,我們需要創建另外一個類,它隻用於保存Person列表,用於存儲成XML文件。

創建的新類名為PersonListWrapper,把它放入到ch.makery.address.model包中。

PersonListWrapper.java
package ch.makery.address.model;

import java.util.List;

import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;

/**
 * Helper class to wrap a list of persons. This is used for saving the
 * list of persons to XML.
 * 
 * @author Marco Jakob
 */
@XmlRootElement(name = "persons")
public class PersonListWrapper {

    private List<Person> persons;

    @XmlElement(name = "person")
    public List<Person> getPersons() {
        return persons;
    }

    public void setPersons(List<Person> persons) {
        this.persons = persons;
    }
}

注意兩個注釋:

  • @XmlRootElement 定義根元素的名稱。
  • @XmlElement 一個可選的名稱,用來指定元素。

使用JAXB讀寫數據

我們讓MainApp類負責讀寫人員數據。添加下麵兩個方法到MainApp.java的最後:

/**
 * Loads person data from the specified file. The current person data will
 * be replaced.
 * 
 * @param file
 */
public void loadPersonDataFromFile(File file) {
    try {
        JAXBContext context = JAXBContext
                .newInstance(PersonListWrapper.class);
        Unmarshaller um = context.createUnmarshaller();

        // Reading XML from the file and unmarshalling.
        PersonListWrapper wrapper = (PersonListWrapper) um.unmarshal(file);

        personData.clear();
        personData.addAll(wrapper.getPersons());

        // Save the file path to the registry.
        setPersonFilePath(file);

    } catch (Exception e) { // catches ANY exception
        Dialogs.create()
                .title("Error")
                .masthead("Could not load data from file:\n" + file.getPath())
                .showException(e);
    }
}

/**
 * Saves the current person data to the specified file.
 * 
 * @param file
 */
public void savePersonDataToFile(File file) {
    try {
        JAXBContext context = JAXBContext
                .newInstance(PersonListWrapper.class);
        Marshaller m = context.createMarshaller();
        m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);

        // Wrapping our person data.
        PersonListWrapper wrapper = new PersonListWrapper();
        wrapper.setPersons(personData);

        // Marshalling and saving XML to the file.
        m.marshal(wrapper, file);

        // Save the file path to the registry.
        setPersonFilePath(file);
    } catch (Exception e) { // catches ANY exception
        Dialogs.create().title("Error")
                .masthead("Could not save data to file:\n" + file.getPath())
                .showException(e);
    }
}

編組和解組已經準備好,讓我們創建保存和加載的菜單實際的使用它。

處理菜單響應

在我們RootLayout.fxml中,這裡已經有一個菜單,但是我們冇有使用它。在我們添加響應到菜單中之前,我們首先創建所有的菜單項。

在Scene Builder中打開RootLayout.fxml,從library組中拖曳必要的菜單到Hierarchy組的MemuBar中。創建NewOpen…SaveSave As…Exit菜單項。

添加菜單項

提示:使用Properties組下的Accelerator設置,你能設置菜單項的快捷鍵。

RootLayoutController

為了處理菜單動作,我們需要創建一個新的控製器類。在控製器包ch.makery.address.view中創建一個類RootLayoutController

添加下麵的內容到控製器中:

RootLayoutController.java
package ch.makery.address.view;

import java.io.File;

import javafx.fxml.FXML;
import javafx.stage.FileChooser;

import org.controlsfx.dialog.Dialogs;

import ch.makery.address.MainApp;

/**
 * The controller for the root layout. The root layout provides the basic
 * application layout containing a menu bar and space where other JavaFX
 * elements can be placed.
 * 
 * @author Marco Jakob
 */
public class RootLayoutController {

    // Reference to the main application
    private MainApp mainApp;

    /**
     * Is called by the main application to give a reference back to itself.
     * 
     * @param mainApp
     */
    public void setMainApp(MainApp mainApp) {
        this.mainApp = mainApp;
    }

    /**
     * Creates an empty address book.
     */
    @FXML
    private void handleNew() {
        mainApp.getPersonData().clear();
        mainApp.setPersonFilePath(null);
    }

    /**
     * Opens a FileChooser to let the user select an address book to load.
     */
    @FXML
    private void handleOpen() {
        FileChooser fileChooser = new FileChooser();

        // Set extension filter
        FileChooser.ExtensionFilter extFilter = new FileChooser.ExtensionFilter(
                "XML files (*.xml)", "*.xml");
        fileChooser.getExtensionFilters().add(extFilter);

        // Show save file dialog
        File file = fileChooser.showOpenDialog(mainApp.getPrimaryStage());

        if (file != null) {
            mainApp.loadPersonDataFromFile(file);
        }
    }

    /**
     * Saves the file to the person file that is currently open. If there is no
     * open file, the "save as" dialog is shown.
     */
    @FXML
    private void handleSave() {
        File personFile = mainApp.getPersonFilePath();
        if (personFile != null) {
            mainApp.savePersonDataToFile(personFile);
        } else {
            handleSaveAs();
        }
    }

    /**
     * Opens a FileChooser to let the user select a file to save to.
     */
    @FXML
    private void handleSaveAs() {
        FileChooser fileChooser = new FileChooser();

        // Set extension filter
        FileChooser.ExtensionFilter extFilter = new FileChooser.ExtensionFilter(
                "XML files (*.xml)", "*.xml");
        fileChooser.getExtensionFilters().add(extFilter);

        // Show save file dialog
        File file = fileChooser.showSaveDialog(mainApp.getPrimaryStage());

        if (file != null) {
            // Make sure it has the correct extension
            if (!file.getPath().endsWith(".xml")) {
                file = new File(file.getPath() + ".xml");
            }
            mainApp.savePersonDataToFile(file);
        }
    }

    /**
     * Opens an about dialog.
     */
    @FXML
    private void handleAbout() {
        Dialogs.create()
            .title("AddressApp")
            .masthead("About")
            .message("Author: Marco Jakob\nWebsite: http://code.makery.ch")
            .showInformation();
    }

    /**
     * Closes the application.
     */
    @FXML
    private void handleExit() {
        System.exit(0);
    }
}

FileChooser

注意在上麵的RootLayoutController中使用FileCooser的方法。首先,創建新的FileChooser類對象的,然後,添加擴展名過濾器,以至於隻顯示以.xml結尾的文件。最後,文件選擇器顯示在主Stage的上麵。

如果用戶冇有選擇一個文件關閉對話框,返回null。否則,我們獲得選擇的文件,我們能傳遞它到MainApploadPersonDataFromFile(…)savePersonDataToFile()方法中。

連接fxml視圖到控製器

  1. 在Scene Builder中打開RootLayout.fxml。在Controller組中選擇RootLayoutController作為控製器類。

  2. 回到Hierarchy組中,選擇一個菜單項。在Code組中On Action下,應該看到所有可用控製器方法的選擇。為每個菜單項選擇響應的方法。
    菜單動作

  3. 為每個菜單項重複第2步。

  4. 關閉Scene Builder,並且在項目的根目錄上按下刷新F5。這讓Eclipse知道在Scene Builder中所做的修改。

連接MainApp和RootLayoutController

在幾個地方,RootLayoutController需要引用MainApp類。我們也冇有傳遞一個MainApp的引用到RootLayoutController

打開MainApp類,使用下麵的替代initRootLayout()方法:

/**
 * Initializes the root layout and tries to load the last opened
 * person file.
 */
public void initRootLayout() {
    try {
        // Load root layout from fxml file.
        FXMLLoader loader = new FXMLLoader();
        loader.setLocation(MainApp.class
                .getResource("view/RootLayout.fxml"));
        rootLayout = (BorderPane) loader.load();

        // Show the scene containing the root layout.
        Scene scene = new Scene(rootLayout);
        primaryStage.setScene(scene);

        // Give the controller access to the main app.
        RootLayoutController controller = loader.getController();
        controller.setMainApp(this);

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

    // Try to load last opened person file.
    File file = getPersonFilePath();
    if (file != null) {
        loadPersonDataFromFile(file);
    }
}

注意兩個修改:一行給控製器訪問MainApp和最後三行加載最新打開的人員文件

測試

做應用程序的測試驅動,你應該能夠使用菜單保存人員數據到文件中。

當你在編輯器中打開一個xml文件,你將注意到生日冇有正確保存,這是一個空的<birthday/>標簽。原因是JAXB不隻奧如何轉換LocalDate到XML。我們必須提供一個自定義的LocalDateAdapter定義這個轉換。

ch.makery.address.util中創建新的類,稱為LocalDateAdapter,內容如下:

LocalDateAdapter.java
package ch.makery.address.util;

import java.time.LocalDate;

import javax.xml.bind.annotation.adapters.XmlAdapter;

/**
 * Adapter (for JAXB) to convert between the LocalDate and the ISO 8601 
 * String representation of the date such as '2012-12-03'.
 * 
 * @author Marco Jakob
 */
public class LocalDateAdapter extends XmlAdapter<String, LocalDate> {

    @Override
    public LocalDate unmarshal(String v) throws Exception {
        return LocalDate.parse(v);
    }

    @Override
    public String marshal(LocalDate v) throws Exception {
        return v.toString();
    }
}

然後打開Person.jar,添加下麵的注釋到getBirthday()方法上:

@XmlJavaTypeAdapter(LocalDateAdapter.class)
public LocalDate getBirthday() {
    return birthday.get();
}

現在,再次測試。試著保存和加載XML文件。在重啟之後,它應該自動加載最後使用的文件。

它如何工作

讓我們看下它是如何一起工作的:

  1. 應用程序使用MainApp中的main(…)方法啟動。
  2. 調用public MainApp()構造函數添加一些樣例數據。
  3. 調用MainAppstart(…)方法,調用initRootLayout()RootLayout.fxml中初始化根布局。fxml文件有關於使用控製器的信息,連接視圖到RootLayoutController
  4. MainApp從fxml加載器中獲取RootLayoutController,傳遞自己的引用到控製器中。使用這些引用,控製器隨後可以訪問MainApp的公開方法。
  5. initRootLayout方法結束,我們試著從Perferences中獲取最後打開的人員文件。如果Perferences知道有這樣一個XML文件,我們將從這個XML文件中加載數據。這顯然會覆蓋掉構造函數中的樣例數據。