只是关于可用性的说明:如果您将“动作”与“视图”完全分开(即,如果您的控制器真的对 UI 组件一无所知),事情可能会变得有点复杂。例如,大部分时间按钮操作都希望查看文本字段的状态等。您当然可以通过使用演示者将文本字段中的文本绑定到表示模型中的数据,然后让控制器调用模型上引用该状态的方法。那么问题是控制器方法基本上除了调用表示模型上的等效方法之外什么都不做。你最终会得到一个真的太薄且无法承受重量的层,并且架构看起来过度设计。
也就是说,如果您确实想对此进行试验,这里有一种可行的方法。
这里的主要障碍是FXMLLoader 有一个与之关联的controller 实例。当它加载 FXML 时,它会将具有fx:id 属性的元素注入控制器,并且将控制器中的“处理程序”方法与通过 FXML 中的onXXX 属性指定的事件处理程序相关联。 p>
实现这项工作的方法是使用FXMLLoader 的namespace,它是从fx:id 值到相应元素的映射。所以我认为可行的方法是使用默认加载过程将处理程序与您的控制器相关联,然后使用一堆反射从命名空间中的值初始化演示者中的@FXML-annotated 字段。
后半部分是这样的:
private void injectFieldsIntoPresenter(FXMLLoader loader, P presenter) throws IllegalArgumentException, IllegalAccessException {
Map<String, Object> namespace = loader.getNamespace() ;
for (Field field : presenter.getClass().getDeclaredFields()) {
boolean wasAccessible = field.isAccessible() ;
field.setAccessible(true);
if (field.getAnnotation(FXML.class) != null) {
if (namespace.containsKey(field.getName())) {
field.set(presenter, namespace.get(field.getName()));
}
}
field.setAccessible(wasAccessible);
}
}
当然,您的演示者还需要执行一些绑定,因此我们需要安排一个方法在字段被注入之后调用。这是由FXMLLoader 通过调用任何public 或@FXML-annotated 称为initialize() 的方法为控制器类完成的;因此,如果您希望演示者具有相同的功能,您可以这样做:
private void initializePresenterIfPossible(P presenter) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
for (Method m : presenter.getClass().getDeclaredMethods()) {
boolean wasAccessible = m.isAccessible() ;
m.setAccessible(true);
if ("initialize".equals(m.getName()) && m.getParameterCount() == 0) {
if ((m.getModifiers() & Modifier.PUBLIC) != 0 || m.getAnnotation(FXML.class) != null) {
m.invoke(presenter);
}
}
m.setAccessible(wasAccessible);
}
}
(您可以在此处使用其他方案,例如使用javax.inject 注释并简单地调用任何@PostConstruct-annotated 方法。)
因此,包装FXMLLoader 并执行这些附加步骤的通用加载类可能如下所示。这有几个额外的功能:因为你的控制器和你的演示者都需要访问模型,它会注入任何类型与模型实例相同类型的 @FXML-annotated 字段。 (同样,您可以根据需要进行修改。)
如您所见,此功能依赖于一大堆反射:它基本上是在实现一个微框架。
package mvpc;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.URL;
import java.util.Map;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
public class MVPCLoader<M, V, P, C> {
private P presenter ;
private C controller ;
private V view ;
private M model ;
public V load(URL resource, M model, P presenter) throws IllegalArgumentException, IllegalAccessException, InvocationTargetException, IOException {
if (view != null) {
throw new IllegalStateException("FXML can only be loaded once by a MVPCLoader instance");
}
this.model = model ;
this.presenter = presenter ;
FXMLLoader loader = new FXMLLoader(resource);
loader.setControllerFactory(this::controllerFactory);
view = loader.load();
controller = loader.getController() ;
injectInto(presenter, model);
injectFieldsIntoPresenter(loader, presenter);
initializePresenterIfPossible(presenter);
return view ;
}
public P getPresenter() {
return presenter ;
}
public M getModel() {
return model ;
}
public C getController() {
return controller ;
}
private void initializePresenterIfPossible(P presenter) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
for (Method m : presenter.getClass().getDeclaredMethods()) {
boolean wasAccessible = m.isAccessible() ;
m.setAccessible(true);
if ("initialize".equals(m.getName()) && m.getParameterCount() == 0) {
if ((m.getModifiers() & Modifier.PUBLIC) != 0 || m.getAnnotation(FXML.class) != null) {
m.invoke(presenter);
}
}
m.setAccessible(wasAccessible);
}
}
private void injectFieldsIntoPresenter(FXMLLoader loader, P presenter) throws IllegalArgumentException, IllegalAccessException {
Map<String, Object> namespace = loader.getNamespace() ;
for (Field field : presenter.getClass().getDeclaredFields()) {
boolean wasAccessible = field.isAccessible() ;
field.setAccessible(true);
if (field.getAnnotation(FXML.class) != null) {
if (namespace.containsKey(field.getName())) {
field.set(presenter, namespace.get(field.getName()));
}
}
field.setAccessible(wasAccessible);
}
}
private C controllerFactory(Class<?> type) {
try {
@SuppressWarnings("unchecked")
C controller = (C) type.newInstance();
injectInto(controller, model);
return controller ;
} catch (Exception exc) {
if (exc instanceof RuntimeException) throw (RuntimeException)exc ;
throw new RuntimeException(exc);
}
}
private void injectInto(Object target, Object value) throws IllegalArgumentException, IllegalAccessException {
for (Field field : target.getClass().getDeclaredFields()) {
boolean wasAccessible = field.isAccessible() ;
field.setAccessible(true);
if (field.get(target) == null && field.getType() == value.getClass() && field.getAnnotation(FXML.class) != null) {
field.set(target, value);
}
field.setAccessible(wasAccessible);
}
}
}
查看afterburner.fx 的源代码启发了执行此操作的技术。
这是一个使用这个类的快速测试:
package mvpc;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
public class PresentationModel {
private final IntegerProperty count = new SimpleIntegerProperty();
public IntegerProperty countProperty() {
return count ;
}
public final int getCount() {
return countProperty().get();
}
public final void setCount(int count) {
countProperty().set(count);
}
public final void increment() {
setCount(getCount() + 1);
}
}
package mvpc;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
public class Presenter {
@FXML
private PresentationModel model ;
@FXML
private Label display ;
public void initialize() {
display.textProperty().bind(model.countProperty().asString("Count: %d"));
}
}
package mvpc;
import javafx.fxml.FXML;
public class Controller {
@FXML
private PresentationModel model ;
@FXML
private void increment() {
model.increment();
}
}
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.VBox?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.Button?>
<VBox xmlns:fx="http://javafx.com/fxml/1" fx:controller="mvpc.Controller" spacing="5" alignment="CENTER">
<padding>
<Insets top="10" left="10" bottom="10" right="10"/>
</padding>
<Label fx:id="display"/>
<Button text="Increment" onAction="#increment"/>
</VBox>
package mvpc;
import javafx.application.Application;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class MVPCTest extends Application {
@Override
public void start(Stage primaryStage) throws Exception {
PresentationModel model = new PresentationModel();
Presenter presenter = new Presenter();
MVPCLoader<PresentationModel, Parent, Presenter, Controller> loader = new MVPCLoader<>();
Scene scene = new Scene(loader.load(getClass().getResource("View.fxml"), model, presenter));
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}