JavaFX - MVPC 模式 - 分离 FXML 对象和事件处理程序方法

JavaFX - MVPC Pattern - separate FXML objects and event handler methods

我想在 JavaFX 中创建一个实现 MVPC 模式的应用程序。 我的想法如下:

正如您所注意到的,我的目标是将 FXML 对象(如@FXML Label 标签)与 PRESENTER 和 FXML 事件处理程序方法(如@FXML submit(Action event e){})分离到 CONTROLLER。

简而言之:我有一个 FXML 文件,其中包含 fx:id="passwordField" 等元素和 onAction="#browseSbx" 等事件处理程序。我想要两个独立的 .java 控制器,一个包含 fx:id 的对象,一个处理事件方法。

我的问题:有什么 "clean" 方法可以做到这一点吗?还是我的计划有概念上的错误?

谢谢!

关于可用性的注意事项:如果您将 "actions" 与 "view" 完全分开(即,如果您的控制器真的对 UI 组件一无所知),事情可能会变得有点费解。例如,很多时候按钮操作会想要查看文本字段的状态等。您当然可以通过使用您的演示者绑定,比如说,文本中的文本来做到这一点文本字段到表示模型中的数据,然后让控制器调用引用该状态的模型上的方法。那么问题是控制器方法基本上除了在表示模型上调用等效方法外什么都不做;你最终得到的一层真的太薄了,没有发挥它的重量,而且架构看起来设计过度。

也就是说,如果您确实想对此进行试验,这里有一种可行的方法。

这里的主要障碍是 FXMLLoader 有一个与之关联的 controller 实例。当它加载 FXML 时,它会将具有 fx:id 属性的元素注入到控制器中, 将控制器中的 "handler" 方法与通过 [=21] 指定的事件处理程序相关联=] FXML 中的属性。

完成这项工作的方法是使用 FXMLLoadernamespace,它是从 fx:id 值到相应元素的映射。因此,我认为可行的方法是使用默认加载过程将处理程序与您的控制器相关联,然后使用一堆反射从命名空间中的值初始化演示者中的 @FXML- 注释字段。

后半部分看起来像:

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 注释方法调用 initialize() 为控制器 class 完成的;所以如果你想为你的演示者提供相同的功能,你可以这样做:

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 注释的方法。)

因此,包装 FXMLLoader 并执行这些附加步骤的通用加载 class 可能如下所示。这有几个额外的功能:因为你的控制器和你的演示者都需要访问模型,它会注入任何 @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.

源代码的启发

下面是一个使用 class:

的快速测试
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);
    }
}