首页 文章

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

提问于
浏览
0

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

  • VIEW:简单的FXML文件

  • CONTROLLER:包含VIEW中定义的事件处理函数,更新演示模型

  • 演示模型:简单数据,包含可观察对象(ObjectProperty,ObservableList等)

  • PRESENTER:包含FXML文件中由fx:id定义的JavaFX节点,将这些节点绑定到PRESENTATION MODEL中的可观察对象,并处理其他演示功能,如弹出窗口 . 这将是JavaFX应用程序 .

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

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

我的问题:这有什么“干净”的方法吗?或者我的计划中有任何概念错误吗?

谢谢!

1 回答

  • 0

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

    也就是说,如果你想尝试这个,这里有一种方法可行 .

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

    你可以使用的方法是使用 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 为控制器类完成的,方法是调用任何名为 initialize()public@FXML 注释方法;因此,如果您希望演示者具有相同的功能,则可以执行以下操作:

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

相关问题