【问题标题】:JavaFX - MVPC Pattern - separate FXML objects and event handler methodsJavaFX - MVPC 模式 - 分离 FXML 对象和事件处理程序方法
【发布时间】:2017-12-22 10:12:43
【问题描述】:

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

  • 查看:简单的 FXML 文件
  • CONTROLLER:包含在 VIEW 中定义的事件处理函数,更新 PRESENTATION MODEL
  • PRESENTATION MODEL:简单数据,包含可观察对象(ObjectProperty、ObservableList 等)
  • PRESENTER:包含由 FXML 文件中的 fx:id 定义的 JavaFX 节点,将这些节点绑定到 PRESENTATION MODEL 中的可观察对象并处理其他表示功能,如弹出窗口。这将是 JavaFX 应用程序。

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

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

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

谢谢!

【问题讨论】:

  • 我不知道我是否非常理解您的问题...您想为视图、控制器、演示模型和演示者创建单独的 .fxml 文件?或者您想将 Java 代码分离到单独的 Java 文件中?
  • 对不起。我只有一个 FXML 文件,其中包含 fx:id="passwordField" 等元素和 onAction="#browseSbx" 等事件处理程序。我想要两个独立的 .java 控制器,一个包含 fx:ids 的对象,一个用于处理事件方法。
  • 然后你必须在你的java类中导入你需要的所有.fxml文件。您可以在 .fxml 类中指定与该 .fxml 文件关联使用的控制器。
  • 正如我所说,我只有一个 .fxml 文件。我需要将两个单独的控制器连接到一个 .fxml 文件。
  • 我不太确定您是否可以将更多控制器关联到您的单个文件,但我建议您使用 GenericController 并在您的 CustomControllers 类中扩展您的 GenericController :)

标签: java model-view-controller javafx fxml


【解决方案1】:

只是关于可用性的说明:如果您将“动作”与“视图”完全分开(即,如果您的控制器真的对 UI 组件一无所知),事情可能会变得有点复杂。例如,大部分时间按钮操作都希望查看文本字段的状态等。您当然可以通过使用演示者将文本字段中的文本绑定到表示模型中的数据,然后让控制器调用模型上引用该状态的方法。那么问题是控制器方法基本上除了调用表示模型上的等效方法之外什么都不做。你最终会得到一个真的太薄且无法承受重量的层,并且架构看起来过度设计。

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

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

实现这项工作的方法是使用FXMLLoadernamespace,它是从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);
    }
}

【讨论】:

  • 哦,非常感谢。这是一个非常好的解决方案。我会深入检查,也会检查 afterburner.fx。
  • 我的基本想法是创建一个框架(因为我没有找到),让我可以在工作中快速轻松地创建小型(但有时在后台非常复杂)桌面工具。跨度>
猜你喜欢
  • 2019-11-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多