JavaFX学习:MVC模式中的PropertyValueFactory

 PropertyValueFactory类是“TableColumn cell value factory”,绑定创建列表中的项。示例如下:

 TableColumn<Person,String> firstNameCol = new TableColumn<Person,String>("First Name");
 firstNameCol.setCellValueFactory(new PropertyValueFactory<Person,String>("firstName"));

在以上中,Person类是TableView视图绑定的列表的项(items),String是TableColumn中项数据的类型。Person类必须是public,“First Name”是在TableView中显示的表头内容。PropertyValueFactory类的构造方法传入参数“firstName”创建实例,在列表项Person类中寻找与对应的无参属性方法firstNameProperty(firstNameProperty方法必须与传入的参数firstName对应,应该是通过反射方式进行名称对应。firstNameProperty方法可以对应任何名称的属性字段,例如firstNameABC属性字段都可以)返回ObservableValue<String>。

如果无参属性方法firstNameProperty存在,则该方法会被触发并返回一个Property<String>(Property是ObservableValue的子接口)类型的实例。返回的实例作为数据项用来填充“Table Cell”。同时,TableView给返回的Property<String>实例注册一个观察者(observer),这样对数据项的任何改变都因触发observer立即通知TableView,项内容立即更新。

如果Person类中没有与“firstName”对应的无参firstNameProperty方法,PropertyValueFactory类则会扫描Person类中是否有返回值是String类型的无参方法getFirstName或无参方法isFirstName。如果有上述两个方法,则方法会被调用,返回被ReadOnlyObjectWrapper包装的值,值填充“Table Cell”。这种情况下,TableCell无法给包装的属性注册观察者观察数据变化状态。这种情况与调用firstNameProperty方法不同。

注:《Learn JavaFX 8 Building User Experience and Interfaces with Java8》书中第13章例子,对于ageCategory数据显示的是Person.AgeCategory(枚举类型)数据,但是在处理过程中感觉是使用String类型数据处理。在后面Person的例子中,新增public SimpleStringProperty ageCategoryProperty()方法、

public ReadOnlyStringWrapper ageCategoryProperty()方法符合PropertyValueFactory类的文档描述使用。

For reference (and as noted in the TableColumn TableColumn.cellValueFactoryProperty() cell value factory} documentation), the long form of the code above would be the following:

以下代码作为参考:

TableColumn<Person,String> firstNameCol = new TableColumn<Person,String>("First Name");

firstNameCol.setCellValueFactory(new Callback<CellDataFeatures<Person, String>, ObservableValue<String>>() {
     public ObservableValue<String> call(CellDataFeatures<Person, String> p) {
         // p.getValue() returns the Person instance for a particular TableView row
         return p.getValue().firstNameProperty();
     }
  });
 }

以下是一个使用PropertyValueFactory的例子:

继承PropertyValueFactory的类,实现方法call:

package cn.learnjavafx.ch13.tableview04;

import java.time.LocalDate;
import static java.time.temporal.ChronoUnit.YEARS;

import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.beans.value.ObservableValue;
import javafx.scene.control.TableColumn.CellDataFeatures;
import javafx.scene.control.cell.PropertyValueFactory;

import cn.learnjavafx.ch11.Person;

/**
 * @copyright 2023-2022
 * @package   cn.learnjavafx.ch13.tableview04
 * @file      ModelDataByPropertyValueFactory.java
 * @date      2023-07-08 22:59
 * @author    qiao wei
 * @version   1.0
 * @brief     继承PropertyValueFactory类,重写call方法,间接实现Callback接口。
 * @history
 */
public class ModelDataByPropertyValueFactory extends PropertyValueFactory<Person, String> {

	public ModelDataByPropertyValueFactory(Person person, String content) {
		super(content);
	}

	@Override
	public ObservableValue<String> call(CellDataFeatures<Person, String> cellDataFeatures) {
		Person person = cellDataFeatures.getValue();
		LocalDate localDate = person.birthDate();
		String ageInYear = "Unknown";

		if (null != localDate) {
			long years = YEARS.between(localDate, LocalDate.now());
			if (years == 0) {
				ageInYear = "< 1 year";
			} else if (years == 1) {
				ageInYear = years + " year";
			} else {
				ageInYear = years + " years";
			}
		}

		return new ReadOnlyStringWrapper(ageInYear);
	}
}

调用ModelDataByPropertyValueFactory类替代PropertyValueFactory类。只看start01方法的调用实现。在start0*方法掉作用中,可以修改Person类提供的ageCategory方法测试提供给PropertyValueFactory构造方的参数。

package learn.javafx8.ch13.tableview04;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
import learn.javafx8.ch11.AgeCategory;
import learn.javafx8.ch11.Person;
import learn.javafx8.ch13.PersonTableUtil;

/**
 * @copyright 2023-2022
 * @package   cn.javafx.ch13.tableview04
 * @file      TableViewDataTest.java
 * @date      2023-07-15 01:56
 * @author    qiao wei
 * @version   1.0
 * @brief     
 * @history
 */
public class TableViewDataTest extends Application {
	
	public static void main(String[] args) {
		Application.launch(TableViewDataTest.class, args);
	}

	@Override
	@SuppressWarnings("unchecked")
	public void start(Stage primaryStage) {
		try {
			start02(primaryStage);
		} catch (Exception exception) {
			exception.printStackTrace();
		}
	}
	
	/**
	 * @class   TableViewDataTest
	 * @date    2023-07-15 09:01
	 * @author  qiao wei
	 * @version 1.0
	 * @brief   ModelDataByPropertyValueFactory类继承PropertyValueFactory类,间接继承Callback回调接口,重写call方法。
	 * @param   
	 * @return  
	 * @throws
	 */
	private void start01(Stage primaryStage) throws Exception {
		// Create a TableView and bind model.
		TableView<Person> table = new TableView<>(PersonTableUtil.getPersonList());

		/**
		 * Create an "Age" computed column.
		 * TableColumn<S, T>(text)
		 *  S: The type of the TableView generic type.
		 *  T: The type of the content in all cells in this TableColumn.
		 *  "Age": The string to show when the TableColumn is placed within the TableView.
		 */
		TableColumn<Person, String> ageColumn = new TableColumn<>("Age");
		ageColumn.setCellValueFactory(new ModelDataByPropertyValueFactory(new Person(), new String()));
		
		// Create an "Age Category" column.
		TableColumn<Person, AgeCategory> ageCategoryColumn = new TableColumn<>("Age Category");
//		ageCategoryColumn.setCellValueFactory(new PropertyValueFactory<>("ageCategory"));
		ageCategoryColumn.setCellValueFactory(new ModelDataByAgeCategory());

		// Add columns to the TableView.
		table.getColumns().addAll(PersonTableUtil.getIdColumn()
			, PersonTableUtil.getFirstNameColumn()
			, PersonTableUtil.getLastNameColumn()
			, PersonTableUtil.getBirthDateColumn()
			, ageColumn
			, ageCategoryColumn);

		HBox root = new HBox(table);
		root.setStyle("-fx-padding: 10;"
			+ "-fx-border-style: solid inside;"
			+ "-fx-border-width: 2;"
			+ "-fx-border-insets: 5;"
			+ "-fx-border-radius: 5;"
			+ "-fx-border-color: blue;");

		Scene scene = new Scene(root);
		primaryStage.setScene(scene);
		primaryStage.setTitle("Populating TableViews");
		primaryStage.show();
	}
	
	/**
	 * @class   TableViewDataTest
	 * @date    2023-07-15 09:12
	 * @author  qiao wei
	 * @version 1.0
	 * @brief   ModelDataByCallback类实现Callback接口。
	 * @param   
	 * @return  
	 * @throws
	 */
	private void start02(Stage primaryStage) throws Exception {
		// Create a TableView and bind model.
		TableView<Person> table = new TableView<>(PersonTableUtil.getPersonList());

		/**
		 * Create an "Age" computed column.
		 * TableColumn<S, T>(text)
		 *  S: The type of the TableView generic type.
		 *  T: The type of the content in all cells in this TableColumn.
		 *  "Age": The string to show when the TableColumn is placed within the TableView.
		 */
		TableColumn<Person, String> ageColumn = new TableColumn<>("Age");
		ageColumn.setCellValueFactory(new ModelDataByCallback());

		
//		TableColumn<Person, AgeCategory> ageCategoryColumn = new TableColumn<>("Age Category");
//		ageCategoryColumn.setCellValueFactory(new PropertyValueFactory<>("ageCategory"));

		TableColumn<Person, String> ageCategoryColumn = new TableColumn<>("Age Category");
		ageCategoryColumn.setCellValueFactory(new PropertyValueFactory<>("ageCategory"));

		// Add columns to the TableView.
		table.getColumns().addAll(PersonTableUtil.getIdColumn()
			, PersonTableUtil.getFirstNameColumn()
			, PersonTableUtil.getLastNameColumn()
			, PersonTableUtil.getBirthDateColumn()
			, ageColumn
			, ageCategoryColumn);

		HBox root = new HBox(table);
		root.setStyle("-fx-padding: 10;"
			+ "-fx-border-style: solid inside;"
			+ "-fx-border-width: 2;"
			+ "-fx-border-insets: 5;"
			+ "-fx-border-radius: 5;"
			+ "-fx-border-color: blue;");

		Scene scene = new Scene(root);
		primaryStage.setScene(scene);
		primaryStage.setTitle("Populating TableViews");
		primaryStage.show();
	}
	
	/**
	 * @class   TableViewDataTest
	 * @date    2023-07-15 09:21
	 * @author  qiao wei
	 * @version 1.0
	 * @brief   ModelDataByAgeCategory类实现Callback接口。call方法的返回值为ObservableValue<AgeCategory>,在call方法中将
	 *           AgeCategory枚举类型包装成ReadOnlyObjectWrapper类型返回。
	 * @param   
	 * @return  
	 * @throws
	 */
	private void start03(Stage primaryStage) throws Exception {
		// Create a TableView and bind model.
		TableView<Person> table = new TableView<>(PersonTableUtil.getPersonList());

		/**
		 * Create an "Age" computed column.
		 * TableColumn<S, T>(text)
		 *  S: The type of the TableView generic type.
		 *  T: The type of the content in all cells in this TableColumn.
		 *  "Age": The string to show when the TableColumn is placed within the TableView.
		 */
		TableColumn<Person, String> ageColumn = new TableColumn<>("Age");
		ageColumn.setCellValueFactory(new ModelDataByCallback());

		/**
		 * Create an "Age Category" column.
		 * 创建Age列,其中项类型是Person类,列类型是Person.AgeCategory。将“ageCategory”作为属性名称传给PropertyValueFactory
		 *  类的构造方法。首先,PropertyValueFactory类在Person类中检索名为“ageCategory”的属性,但是Person类中没有该属性。
		 *  PropertyValueFactory类按照POJO原则(简答Java原则)处理该属性。PropertyValueFactory类在Person类中寻找
		 *  getAgeCategory方法和setAgeCategory方法,如果只检索到getAgeCategory方法,则该列设置为只读。
		 */
		TableColumn<Person, AgeCategory> ageCategoryColumn = new TableColumn<>("Age Category");
//		ageCategoryColumn.setCellValueFactory(new PropertyValueFactory<>("ageCategory"));
		ageCategoryColumn.setCellValueFactory(new ModelDataByAgeCategory());

		// Add columns to the TableView.
		table.getColumns().addAll(PersonTableUtil.getIdColumn()
			, PersonTableUtil.getFirstNameColumn()
			, PersonTableUtil.getLastNameColumn()
			, PersonTableUtil.getBirthDateColumn()
			, ageColumn
			, ageCategoryColumn);

		HBox root = new HBox(table);
		root.setStyle("-fx-padding: 10;"
			+ "-fx-border-style: solid inside;"
			+ "-fx-border-width: 2;"
			+ "-fx-border-insets: 5;"
			+ "-fx-border-radius: 5;"
			+ "-fx-border-color: blue;");

		Scene scene = new Scene(root);
		primaryStage.setScene(scene);
		primaryStage.setTitle("Populating TableViews");
		primaryStage.show();
	}
}

作为模型数据的Person:

package learn.javafx8.ch11;

import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyIntegerProperty;
import javafx.beans.property.ReadOnlyIntegerWrapper;
import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;

/**
 * @copyright 2023-2022
 * @package   cn.javafx.ch11
 * @file      Person.java
 * @date      2023-07-01 21:31
 * @author    qiao wei
 * @version   1.0
 * @brief     模型类。保存数据,字段使用属性,可以通过注册监听器监听数据更新情况,自动更新属性。
 * @history
 */
public class Person {

	/**
	 * @class   Person
	 * @date    2023-07-01 21:31
	 * @author  qiao wei
	 * @version 1.0
	 * @brief   Default constructor.
	 * @param   
	 * @return  
	 * @throws
	 */
	public Person() {
		this("None", "None", null);
	}

	/**
	 * @class   Person
	 * @date    2023-07-01 21:32
	 * @author  qiao wei
	 * @version 1.0
	 * @brief   Constructor.
	 * @param   firstName 名。
	 * @param   lastName  姓。
	 * @param   birthDate 出生日期。
	 * @return
	 * @throws
	 */
	public Person(String firstName, String lastName, LocalDate birthDate) {
		this.firstName.set(firstName);
		this.lastName.set(lastName);
		this.birthDate.set(birthDate);
	}
	
	public final int personId() {
		return personId.get();
	}
	
	public final ReadOnlyIntegerProperty personIdProperty() {
		return personId.getReadOnlyProperty();
	}

	/**
	 * @class   Person
	 * @date    2023-07-01 21:37
	 * @author  qiao wei
	 * @version 1.0
	 * @brief   Retrieve first name property.
	 * @param
	 * @return  Person first name.
	 * @throws
	 */
	public final String firstName() {
		return firstName.get();
	}

	public final void setFirstName(String firstName) {
		this.firstName.set(firstName);
	}

	public final StringProperty firstNameProperty() {
		return firstName;
	}

	public final String lastName() {
		return lastName.get();
	}

	public final void setLastName(String lastName) {
		this.lastName.set(lastName);
	}

	public final StringProperty lastNameProperty() {
		return lastName;
	}

	/** birthDate Property */
	public final LocalDate birthDate() {
		return birthDate.get();
	}
	
	public final void setBirthDate(LocalDate birthDate) {
		this.birthDate.set(birthDate);
	}
	
	public final ObjectProperty<LocalDate> birthDateProperty() {
		return birthDate;
	}

	/**
	 * @class   Person
	 * @date    2023-07-02 10:30
	 * @author  qiao wei
	 * @version 1.0
	 * @brief   Domain specific business rules.
	 * @param   localDate 当地时区日期
	 * @return
	 * @throws
	 */
	public boolean isValidBirthDate(LocalDate localDate) {
		return isValidBirthDate(localDate, new ArrayList<>());
	}
	
	/**
	 * @class   Person
	 * @date    2023-07-08 19:21
	 * @author  qiao wei
	 * @version 1.0
	 * @brief   验证输入的出生日期是否有效。出生日期无效时,将错误日志记录到errorList中。
	 * @param   date 出生日期。
	 * @param   errorList 错误日志。   
	 * @return  出生日期有效返回true,反之返回false。
	 * @throws
	 */
	public boolean isValidBirthDate(LocalDate date, List<String> errorList) {
		if (null == date) {
			return true;
		}

		// Birthdate cannot be in the future
		if (date.isAfter(LocalDate.now())) {
			errorList.add(LocalDate.now().toString() + " : Birth date must not be in future.");

			return false;
		}

		return true;
	}

	/**
	 * @class   Person
	 * @date    2023-07-02 11:51
	 * @author  qiao wei
	 * @version 1.0
	 * @brief   重写方法,验证个人信息是否正确。Domain specific business rules。
	 * @param   errorList 错误信息列表。
	 * @return
	 * @throws
	 */
	public boolean isValidPerson(List<String> errorList) {
		return isValidPerson(this, errorList);
	}

	/**
	 * @class   Person
	 * @date    2023-07-02 11:53
	 * @author  qiao wei
	 * @version 1.0
	 * @brief   重写方法,验证个人信息是否正确,对个人的姓、名、生日进行有效性验证。Domain specific business
	 *           rules。
	 * @param   person 需要验证的个人信息。
	 * @param   errorList 错误信息列表。记录错误信息。 
	 * @return  true 个人信息有效;false 个人信息无效。
	 * @throws
	 */
	public boolean isValidPerson(Person person, List<String> errorList) {
		boolean isValidPerson = true;
		String firstName = person.firstName();

		// 将以下3个判断条件都走一遍,将所有异常信息统计到errorList中
		if (firstName == null || firstName.trim().length() == 0) {
			errorList.add("First name must contain minimum one character.");
			isValidPerson = false;
		}

		String lastName = person.lastName();
		if (null == lastName || 0 == lastName.trim().length()) {
			errorList.add("Last name must contain minimum one character.");
			isValidPerson = false;
		}

		if ( !isValidBirthDate(this.birthDate.get(), errorList)) {
			isValidPerson = false;
		}

		return isValidPerson;
	}
	
	/**
	 * @class   Person
	 * @date    2023-07-02 12:15
	 * @author  qiao wei
	 * @version 1.0
	 * @brief   根据年龄,返回不同的年龄层。
	 * @param
	 * @return  年龄层,枚举类型。根据不同年龄返回不同年龄层。
	 * @throws
	 */
	public AgeCategory getAgeCategory() {
		if (null == birthDate.get()) {
			return AgeCategory.UNKNOWN;
		}

		// 计算年龄。
		long years = ChronoUnit.YEARS.between(birthDate.get(), LocalDate.now());
		if (0 <= years && 2 > years) {
			return AgeCategory.BABY;
		} else if (2 <= years && 13 > years) {
			return AgeCategory.CHILD;
		} else if (13 <= years && 19 >= years) {
			return AgeCategory.TEEN;
		} else if (19 < years && 50 >= years) {
			return AgeCategory.ADULT;
		} else if (50 < years) {
			return AgeCategory.SENIOR;
		} else {
			return AgeCategory.UNKNOWN;
		}
	}

	/**
	 * @class   Person
	 * @date    2023-07-21 00:29
	 * @author  qiao wei
	 * @version 1.0
	 * @brief   方法命名符合***Property格式,PropertyValueFactory类构造方法通过反射调用。返回值继承接口ObservableValue<String>
	 * @param
	 * @return
	 * @throws
	 */
	public ReadOnlyStringWrapper ageCategoryProperty() {
		if (null == birthDate.get()) {
			return new ReadOnlyStringWrapper(AgeCategory.UNKNOWN.toString());
		}

		// 计算年龄。
		long years = ChronoUnit.YEARS.between(birthDate.get(), LocalDate.now());
		if (0 <= years && 2 > years) {
			return new ReadOnlyStringWrapper(AgeCategory.BABY.toString());
		} else if (2 <= years && 13 > years) {
			return new ReadOnlyStringWrapper(AgeCategory.CHILD.toString());
		} else if (13 <= years && 19 >= years) {
			return new ReadOnlyStringWrapper(AgeCategory.TEEN.toString());
		} else if (19 < years && 50 >= years) {
			return new ReadOnlyStringWrapper(AgeCategory.ADULT.toString());
		} else if (50 < years) {
			return new ReadOnlyStringWrapper(AgeCategory.SENIOR.toString());
		} else {
			return new ReadOnlyStringWrapper(AgeCategory.UNKNOWN.toString());
		}
	}

	/**
	 * @class   Person
	 * @date    2023-07-21 00:29
	 * @author  qiao wei
	 * @version 1.0
	 * @brief   方法命名符合***Property格式,PropertyValueFactory类构造方法通过反射调用。返回值继承接口ObservableValue<String>
	 * @param
	 * @return
	 * @throws
	 */
//	public SimpleStringProperty ageCategoryProperty() {
//		if (null == birthDate.get()) {
//			return new SimpleStringProperty(AgeCategory.UNKNOWN.toString());
//		}
//
//		// 计算年龄。
//		long years = ChronoUnit.YEARS.between(birthDate.get(), LocalDate.now());
//		if (0 <= years && 2 > years) {
//			return new SimpleStringProperty(AgeCategory.BABY.toString());
//		} else if (2 <= years && 13 > years) {
//			return new SimpleStringProperty(AgeCategory.CHILD.toString());
//		} else if (13 <= years && 19 >= years) {
//			return new SimpleStringProperty(AgeCategory.TEEN.toString());
//		} else if (19 < years && 50 >= years) {
//			return new SimpleStringProperty(AgeCategory.ADULT.toString());
//		} else if (50 < years) {
//			return new SimpleStringProperty(AgeCategory.SENIOR.toString());
//		} else {
//			return new SimpleStringProperty(AgeCategory.UNKNOWN.toString());
//		}
//	}

	public boolean save(List<String> errorList) {
		boolean isSaved = false;

		if (isValidPerson(errorList)) {
			System.out.println("Saved " + this.toString());
			isSaved = true;
		}

		return isSaved;
	}

	@Override
	public String toString() {
		StringBuilder stringBuilder = new StringBuilder("[personId=");
		stringBuilder.append(personId.get()).
			append(", firstName = ").append(firstName.get()).
			append(", lastName = ").append(lastName.get()).
			append(", birthDate = ").append(birthDate.get()).append("]");

		return stringBuilder.toString();
	}
	
	/**
	 * @date   2023-07-01 21:33
	 * @author qiao wei
	 * @brief  Person id
	 */
	private final ReadOnlyIntegerWrapper personId =
		new ReadOnlyIntegerWrapper(this, "personId", personSequence.incrementAndGet());
	
	private final StringProperty firstName =
		new SimpleStringProperty(this, "firstName", null);
	
	private final StringProperty lastName =
		new SimpleStringProperty(this, "lastName", null);
	
	/**
	 * @date   2023-07-01 21:33
	 * @author qiao wei
	 * @brief  出生日期。
	 */
	private final ObjectProperty<LocalDate> birthDate =
		new SimpleObjectProperty<>(this, "birthDate", null);
	
	/**
	 * @date   2023-07-01 21:34
	 * @author qiao wei
	 * @brief  Class field. Keeps track of last generated person id.
	 */
	private static AtomicInteger personSequence = new AtomicInteger(0);
}

AgeCategory从Person的内部数据调整。

AgeCategory:

package cn.learnjavafx.ch11;

/**
 * @copyright 2023-2022
 * @package   cn.learnjavafx.ch11
 * @file      AgeCategory.java
 * @date      2023-07-07 16:21
 * @author    qiao wei
 * @version   1.0
 * @brief     年龄段。枚举类型,划分不同年龄段人员。
 * @history
 */
public enum AgeCategory {
	
	BABY,
	
	CHILD,
	
	TEEN,
	
	ADULT,
	
	SENIOR,
	
	UNKNOWN
}

运行结果:

猜你喜欢

转载自blog.csdn.net/weiweiqiao/article/details/131618097