JavaFX - MVPC模式 - 单独的FXML对象和事件处理程序方法

时间:2017-12-22 10:12:43

标签: java model-view-controller javafx fxml

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

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

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

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

我的问题:有没有"清洁"这样做的方法?或者我的计划中是否存在任何概念错误?

谢谢!

1 个答案:

答案 0 :(得分:0)

关于可用性的说明:如果你完全分离出"行动"从"视图" (即如果你的控制器真的对UI组件一无所知),事情可能会有点复杂。例如,大多数时候按钮操作都要查看文本字段的状态等。您当然可以通过使用您的演示者将文本字段中的文本绑定到来执行此操作表示模型中的数据,然后让控制器在引用该状态的模型上调用方法。那么问题是控制器方法除了在表示模型上调用等效方法之外基本上什么都不做;你最终会得到一个实在太薄的层,并且没有拉动它的重量,而且这个架构看起来过于设计。

那就是说,如果你想试试这个,这里有一种方法可行。

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

您可以使用FXMLLoader的{​​{1}} namespace,这是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的{​​{1}}或@FXML注释方法,为控制器类完成此操作;因此,如果您希望演示者具有相同的功能,则可以执行以下操作:

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并执行这些附加步骤的通用加载类可能如下所示。这还有一些额外的功能:由于您的控制器和演示者都需要访问模型,它会注入任何FXMLLoader - 带注释的字段,其类型与模型实例的模型类型相同。 (同样,您可以根据需要进行修改。) 正如您所看到的,此功能依赖于一大堆反思:它基本上是在实现一个微框架。

@FXML

通过查看afterburner.fx的源代码,启发了这样做的技巧。

这是使用此课程的快速测试:

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);
        }
    }
}
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>