在 MVC 模式中处理文本字段 SetOnAction 的正确方法是什么

What is the correct way to handle the Textfield SetOnAction in a MVC pattern

我在尝试让我的 TextField 连接到我在控制器 class 中定义的 ActionEvent 句柄时遇到问题。错误出现在java.lang.reflect.InvocationTargetException。我一直在尝试做的是在视图中创建我的控制器 class 的实例,然后使用 lamba 方法引用来调用控制器 class.

中的 handle 方法

查看Class

package converter;

import javafx.scene.control.RadioButton;
import javafx.scene.control.TextField;
import javafx.scene.control.ToggleGroup;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;

public class View extends BorderPane{
    
    private Controller control = new Controller(new Model(), new View());
    
    //TextField
    private TextField input = new TextField();
    private TextField input2 = new TextField();

    //RadioButton
    private RadioButton distence = new RadioButton();
    private RadioButton tempeture = new RadioButton();
    private RadioButton weight = new RadioButton();
    //ToggleGroup
    private ToggleGroup group = new ToggleGroup();


    public String getConversion() 
    {
        return group.getSelectedToggle().getUserData().toString();
    }

    public double getInput() 
    {
        return Double.parseDouble(input.getText());
    }
    public double getInput2() 
    {
        return Double.parseDouble(input2.getText());
    }

    public void setInput(double value) 
    {
        input.setText(Double.toString(value));
    }

    public void setInput2(double value) 
    {
        input2.setText(Double.toString(value));
    }
    

    
    public View() 
    {
        
        System.out.println(control);
        //SetID
        //input.setPromptText("Input");
        //output.setPromptText("Output");
        
        input.setId("input");
        input2.setId("input2");

        //SetUserData
        distence.setUserData("dist");
        tempeture.setUserData("temp");
        weight.setUserData("weight");
        
        //Set Label
        distence.setText("Mile and Kilometer");
        tempeture.setText("Celsius and Fahrenheit");
        weight.setText("Pounds and Kilograms");

        //SetGroup
        distence.setToggleGroup(group);
        tempeture.setToggleGroup(group);
        weight.setToggleGroup(group);
        
        //Add TextField ActionEvent
        input.setOnAction(control::handle);
        input2.setOnAction(control::handle);
        
        //Add Group Listener
        group.selectedToggleProperty().addListener((ov, o , n) ->{
//          System.out.println(n.getUserData().toString());
            String tog = n.getUserData().toString();
            if (tog.equals("dist")) {
                
                input.setPromptText("Mile");
                input2.setPromptText("Kilometer");
                
            }else if(tog.equals("temp")) {
                input.setPromptText("Fahrenheit");
                input2.setPromptText("Celsius");
            }else if(tog.equals("weight")) {
                input.setPromptText("Pound");
                input2.setPromptText("Kilogram");
            }
        });
        
        StackPane left = new StackPane();
        StackPane right = new StackPane();

        VBox leftbox = new VBox(3);
        VBox rightbox = new VBox(2);

        leftbox.setSpacing(5);
        rightbox.setSpacing(10);

        leftbox.getChildren().addAll(distence, tempeture, weight);
        left.getChildren().add(leftbox);

        rightbox.getChildren().addAll(input, input2);
        right.getChildren().add(rightbox);

        this.setLeft(left);
        this.setRight(right);
        
    
    }

}

控制器Class

public class Controller implements EventHandler<ActionEvent>{
    private Model model;
    private View view;

    public Controller(Model model, View view) 
    {
        this.model = model;
        this.view = view;
    }
    
    public Controller() 
    {
        initalize();
    }
    
    public Controller initalize() 
    {
        this.model = new Model();
        this.view = new View();
        
        return this;
    }
    
    @Override
    public void handle(ActionEvent event) 
    {
        String id = ((javafx.scene.Node)event.getSource()).getId();
        String conversion = view.getConversion();

        switch (id) {
        case "input":

            if(conversion.equals("dist")) {
                view.setInput2(model.kilometer(view.getInput()));
                break;
            }else if(conversion.equals("temp")) {
                view.setInput2(model.cToF(view.getInput()));
                break;
            }else if(conversion.equals("weight")) {
                view.setInput2(model.kilogram(view.getInput()));
                break;
            }
        case "input2":

            if(conversion.equals("dist")) {
                view.setInput(model.mile(view.getInput2()));
                break;
            }else if(conversion.equals("temp")) {
                view.setInput(model.fToC(view.getInput2()));
                break;
            }else if(conversion.equals("weight")) {
                view.setInput(model.pound(view.getInput2()));
                break;
            }

        default:
            break;
        }
    }

}

主要class

package converter;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class Main extends Application {
    private View view;
    
    @Override
    public void init() 
    {
        Model model = new Model();
        view = new View();
        new Controller(model, view);
    }

    @Override
    public void start(Stage primaryStage) {
        try {
            primaryStage.setMinWidth(350);
            primaryStage.setMinHeight(150);
            
            primaryStage.setScene(new Scene(view));
            primaryStage.show();
        } catch (Exception e) {
            e.printStackTrace();
        }
        
    }

    public static void main(String[] args) {
        launch(args);
    }
}

堆栈跟踪

Exception in Application init method
java.lang.reflect.InvocationTargetException
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:78)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:567)
    at javafx.graphics/com.sun.javafx.application.LauncherImpl.launchApplicationWithArgs(LauncherImpl.java:465)
    at javafx.graphics/com.sun.javafx.application.LauncherImpl.launchApplication(LauncherImpl.java:364)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:78)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:567)
    at java.base/sun.launcher.LauncherHelper$FXHelper.main(LauncherHelper.java:1071)
Caused by: java.lang.RuntimeException: Exception in Application init method
    at javafx.graphics/com.sun.javafx.application.LauncherImpl.launchApplication1(LauncherImpl.java:896)
    at javafx.graphics/com.sun.javafx.application.LauncherImpl.lambda$launchApplication(LauncherImpl.java:196)
    at java.base/java.lang.Thread.run(Thread.java:831)
Caused by: java.lang.WhosebugError
    at javafx.graphics/javafx.scene.Node.getScene(Node.java:1148)
    at javafx.graphics/javafx.scene.Node.updateCanReceiveFocus(Node.java:8502)
    at javafx.graphics/javafx.scene.Node.setTreeVisible(Node.java:8420)
    at javafx.graphics/javafx.scene.Node.updateTreeVisible(Node.java:8411)
    at javafx.graphics/javafx.scene.Node.<init>(Node.java:2596)
    at javafx.graphics/javafx.scene.Parent.<init>(Parent.java:1418)
    at javafx.graphics/javafx.scene.layout.Region.<init>(Region.java:627)
    at javafx.graphics/javafx.scene.layout.Pane.<init>(Pane.java:136)
    at javafx.graphics/javafx.scene.layout.BorderPane.<init>(BorderPane.java:219)
    at converter.View.<init>(View.java:52)
    at converter.View.<init>(View.java:12)
    at converter.View.<init>(View.java:12)
    at converter.View.<init>(View.java:12)
    at converter.View.<init>(View.java:12)
    at converter.View.<init>(View.java:12)
    at converter.View.<init>(View.java:12)
    at converter.View.<init>(View.java:12)
    at converter.View.<init>(View.java:12)
    at converter.View.<init>(View.java:12)
    at converter.View.<init>(View.java:12)
    at converter.View.<init>(View.java:12)
    at converter.View.<init>(View.java:12)
    at converter.View.<init>(View.java:12)
    at converter.View.<init>(View.java:12)
    at converter.View.<init>(View.java:12)
    at converter.View.<init>(View.java:12)
    at converter.View.<init>(View.java:12)
    at converter.View.<init>(View.java:12)
    at converter.View.<init>(View.java:12)
    at converter.View.<init>(View.java:12)
    at converter.View.<init>(View.java:12)
    at converter.View.<init>(View.java:12)
    at converter.View.<init>(View.java:12)
    at converter.View.<init>(View.java:12)
    at converter.View.<init>(View.java:12)
    at converter.View.<init>(View.java:12)
    at converter.View.<init>(View.java:12)
    at converter.View.<init>(View.java:12)
    at converter.View.<init>(View.java:12)
    at converter.View.<init>(View.java:12)
    at converter.View.<init>(View.java:12)
    at converter.View.<init>(View.java:12)
    at converter.View.<init>(View.java:12)
    at converter.View.<init>(View.java:12)
    at converter.View.<init>(View.java:12)
    at converter.View.<init>(View.java:12)
    at converter.View.<init>(View.java:12)
    at converter.View.<init>(View.java:12)
    at converter.View.<init>(View.java:12)
    at converter.View.<init>(View.java:12)
    at converter.View.<init>(View.java:12)
    at converter.View.<init>(View.java:12)
    at converter.View.<init>(View.java:12)
    at converter.View.<init>(View.java:12)
    at converter.View.<init>(View.java:12)
    at converter.View.<init>(View.java:12)
    at converter.View.<init>(View.java:12)
    at converter.View.<init>(View.java:12)
    at converter.View.<init>(View.java:12)
    at converter.View.<init>(View.java:12)
    at converter.View.<init>(View.java:12)
Exception running application converter.Main

您正在此处创建循环依赖。控制器依赖于视图依赖于控制器。

这是糟糕的设计。

您应该实现控制反转并依赖于抽象。抽象要么在其构造中赋予视图,要么存在工厂或某些依赖注入框架。另一种选择是让您的视图发出控制器订阅的视图特定事件。有很多方法可以正确解决这个问题 - 循环依赖不是其中之一。

抽象应该是尽可能小的完全抽象(接口隔离原则),所以如果你需要一个处理程序,你可能有一个接口只有一个handle(event)函数,没有更多。

这并不是禁止Controller实现handle函数,可能会违反单一职责原则,但这要看controller的问题和实现来决定。

在此处查看 SOLID 原则:Wikipedia SOLID

更新:您快完成了,只需将 setEventHandler 添加为视图 属性。

public void setEventHandler(@NotNull EventHandler handler) {
    this.handler = handler;
    updateHandler();
}

private void updateHandler() {
 //Add TextField ActionEvent
 input.setOnAction(handler::handle);
 input2.setOnAction(handler::handle);
}

并将其添加到您的 Main::init:

Controller controller = new Controller(model , view);
view.setEventHandler(controller);

您还必须修复控制器的默认构造函数。

您的异常发生是因为您在构造 View 实例时有无限递归:

public class View {

    // ...

    private Controller control = new Controller(new Model(), new View());

    // ...
}

当您创建一个 View 实例时,您会尝试创建一个新的 View 实例(以传递给 Controller 构造函数),而后者又会创建一个新的 View 实例传递给 Controller 构造函数,等等。即使你在这里解决了依赖关系,你也希望这些对象引用同一个实例;您不想到处创建新实例。

MVC 有几种不同的变体。看起来您正在尝试实现“传统”MVC,其中:

  • 视图观察模型,并在模型发生变化时进行更新
  • 视图将用户对其封装的组件(例如文本字段)的操作委托给控制器
  • 控制器更新模型

所以:

  • 视图应该引用控制器和模型
  • 控制器应该引用模型
  • 模型应该对视图或控制器一无所知

我还建议不要让控制器实现任何 EventHandler 接口;只需定义处理用户输入所需的方法。为每个用户操作定义单独的方法,而不是使用一个单一的 handle() 方法和无穷无尽的 switchif-else 语句。

所以像这样:

public class View { 

    private final Controller controller ;
    private final Model model ;

    private final TextField input ;
    private final TextField input2 ;

    // ...

    public View(Model model, Controller controller) {
        this.model = model ;
        this.controller = controller ;

        input = new TextField();
        input2 = new TextField();

        model.someProperty().addListener((obs, oldValue, newValue) -> {
            /* update controls */
        });

        model.someOtherProperty().addListener((obs, oldValue, newValue) -> {
            /* update controls */
        });

        input.setOnAction(event -> controller.handleInput(input.getText()));
        input2.setOnAction(event -> controller.handleInput2(input2.getText()));

        // layout etc

    }
}
public class Controller {

    public final Model model ;

    public Controller(Model model) {
         this.model = model ;
    }

    public void handleInput(String input) {
        model.setSomeValue(input); 
    }

    public void handleInput2(String input) {
        model.setSomeOtherValue(input2);
    }

    // etc
}

然后你 assemble 使用类似

的代码
Model model = new Model();
Controller controller = new Controller(model);
View view = new View(model, controller);

有关完整示例,请参阅