跳至主要内容

校验与绑定数据

在 ChatGPT 中打开

您的应用程序可以使用 FormView 来编辑现有客户数据。此步骤使用 数据绑定,它将 UI 组件直接连接到数据模型,以实现自动值同步。这减少了您应用程序中的样板代码,并允许您对 Spring 实体 Customer 添加验证检查,使用户在填写表单时提供完整和准确的信息。此步骤涵盖以下概念:

完成此步骤创建了一个版本的 5-validating-and-binding-data。

运行应用程序​

在您开发应用程序时,可以使用 5-validating-and-binding-data 作为比较。要查看应用的运行情况:

  1. 导航到包含 pom.xml 文件的顶级目录,如果您跟随 GitHub 上的版本,这是 5-validating-and-binding-data。

  2. 使用以下 Maven 命令在本地运行 Spring Boot 应用:

    mvn

运行该应用程序会自动在 http://localhost:8080 打开一个新浏览器。

定义验证规则​

开发具有可编辑数据的应用程序时,应该包含验证。验证检查帮助维护用户提交的数据的意义和准确性。如果不进行检查,可能会导致问题,因此重要的是实时捕捉用户在填写表单时可能出现的错误。

由于不同属性的有效性标准可能不同,因此您需要定义每个属性的有效标准,并在有无效内容时通知用户。幸运的是,您可以使用 Jakarta 验证 轻松做到这一点。Jakarta 验证允许您通过注解向属性添加约束。

本教程使用两个 Jakarta 注解,@NotEmpty 和 @Pattern。@NotEmpty 用于检查 null 和空字符串,而 @Pattern 用于检查属性是否与您设置的正则表达式匹配。两个注解都允许您添加消息,以便在属性变为无效时显示。

要要求名和姓都是必填的,且只包含字母,同时使公司名称是可选的并允许字母、数字和空格,请将以下注解应用于 Customer 实体:

@Entity
@Table(name = "customers")
public class Customer {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@NotEmpty(message = "Customer first name is required")
@Pattern(regexp = "[a-zA-Z]*", message = "Invalid characters")
private String firstName = "";

@NotEmpty(message = "Customer last name is required")
@Pattern(regexp = "[a-zA-Z]*", message = "Invalid characters")
private String lastName = "";

@Pattern(regexp = "[a-zA-Z0-9 ]*", message = "Invalid characters")
private String company = "";

private Country country = Country.UNKNOWN;

public enum Country {
UNKNOWN,
GERMANY,
ENGLAND,
ITALY,
USA
}

public Customer(String firstName, String lastName, String company, Country country) {
setFirstName(firstName);
setLastName(lastName);
setCompany(company);
setCountry(country);
}

public Customer(String firstName, String lastName, String company) {
this(firstName, lastName, company, Country.UNKNOWN);
}

public Customer(String firstName, String lastName) {
this(firstName, lastName, "");
}

public Customer(String firstName) {
this(firstName, "");
}

public Customer() {
}

public void setFirstName(String newName) {
firstName = newName;
}

public String getFirstName() {
return firstName;
}

public void setLastName(String newName) {
lastName = newName;
}

public String getLastName() {
return lastName;
}

public void setCompany(String newCompany) {
company = newCompany;
}

public String getCompany() {
return company;
}

public void setCountry(Country newCountry) {
country = newCountry;
}

public Country getCountry() {
return country;
}

public Long getId() {
return id;
}

}

请参阅 Jakarta Bean 验证约束参考 获取完整的验证列表,或从 webforJ Jakarta 验证文章 了解更多信息。

绑定字段​

要在 FormView 中使用 Customer 的验证检查进行数据绑定,您需要创建一个 BindingContext。在数据绑定之前,FormView 中的每个字段都需要一个事件监听器来手动与 Spring 实体 Customer 同步。在 FormView 中创建一个 BindingContext 将 Customer 数据模型绑定并自动同步到 UI 组件。

创建 BindingContext​

BindingContext 的实例需要与之同步的 Spring bean。在 FormView 中,使用 Customer 实体声明 BindingContext:

public class FormView extends Composite<Div> implements WillEnterObserver {
private final CustomerService customerService;

private BindingContext<Customer> context;

Customer customer = new Customer();

然后,为了根据组件的名称自动将 UI 组件绑定到 bean 属性,使用 BindingContext.of(),并提供以下参数:

  • this : 之前,您将 context 声明为 BindingContext。第一个参数设置包含可绑定组件的对象。
  • Customer.class : 第二个参数是用于绑定的 bean 的类。
  • true : 第三个参数启用 Jakarta 验证,允许上下文使用您为 Customer 设置的验证。这样将改变无效组件的样式,并显示设置的消息。

所有这些将形成以下代码行:

context = BindingContext.of(this, Customer.class, true);

使表单响应​

通过数据绑定,您的应用程序现在会自动执行验证检查。通过为检查添加事件监听器,您可以防止用户提交无效的表单。添加以下内容,使提交按钮仅在表单有效时激活:

context.onValidate(e -> submit.setEnabled(e.isValid()));

删除组件的事件监听器​

每次 UI 更改现在都自动与 BindingContext 同步。这意味着您现在可以删除每个字段的事件监听器:

之前

// Without data binding
TextField firstName = new TextField("First Name", e -> customer.setFirstName(e.getValue()));
TextField lastName = new TextField("Last Name", e -> customer.setLastName(e.getValue()));
TextField company = new TextField("Company", e -> customer.setCompany(e.getValue()));
ChoiceBox country = new ChoiceBox("Country",
e -> customer.setCountry(Country.valueOf(e.getSelectedItem().getText())));

之后

// With data binding
TextField firstName = new TextField("First Name");
TextField lastName = new TextField("Last Name");
TextField company = new TextField("Company");
ChoiceBox country = new ChoiceBox("Country");

按属性名称绑定​

由于每个组件的名称与数据模型匹配,webforJ 应用了 自动绑定。如果名称不匹配,则可以使用 @UseProperty 注解进行映射。

@UseProperty("firstName")
TextField firstNameField = new TextField("First Name");

在 fillForm() 方法中读取数据​

之前,在 fillForm() 方法中,您通过手动从 Customer 副本中检索数据初始化每个组件的值。但现在,由于您正在使用 BindingContext,可以使用 read() 方法。该方法将每个绑定组件填充与 Customer 副本中的关联属性。

在 fillForm() 方法中,将 setValue() 方法替换为 read():

public void fillForm(Long customerId) {
customer = customerService.getCustomerByKey(customerId);
context.read(customer);
}

向 submitCustomer() 添加验证​

此步骤中对 FormView 的最后更改是向 submitCustomer() 方法添加防护措施。在提交对 H2 数据库的更改之前,应用程序将使用 write() 方法对绑定上下文的结果执行最终验证。

write() 方法使用 BindingContext 中绑定的 UI 组件更新 bean 的属性,并返回一个 ValidationResult。

使用 write() 方法将绑定组件写入 Customer 副本。如果返回的 ValidationResult 有效,则使用写入的数据更新 H2 数据库。

private void submitCustomer() {
ValidationResult results = context.write(customer);
if (results.isValid()) {
if (customerService.doesCustomerExist(customerId)) {
customerService.updateCustomer(customer);
} else {
customerService.createCustomer(customer);
}
navigateToMain();
}
}

完成的 FormView​

通过这些更改,FormView 的外观如下。应用程序现在支持使用 Spring Boot 和 webforJ 的数据绑定和验证。表单输入与模型自动同步,并根据验证规则进行检查。

@Route("customer/:id?<[0-9]+>")
@FrameTitle("Customer Form")
public class FormView extends Composite<Div> implements WillEnterObserver {
private final CustomerService customerService;
private BindingContext<Customer> context;
private Customer customer = new Customer();
private Long customerId = 0L;
private Div self = getBoundComponent();
private TextField firstName = new TextField("First Name");
private TextField lastName = new TextField("Last Name");
private TextField company = new TextField("Company");
private ChoiceBox country = new ChoiceBox("Country");
private Button submit = new Button("Submit", ButtonTheme.PRIMARY, e -> submitCustomer());
private Button cancel = new Button("Cancel", ButtonTheme.OUTLINED_PRIMARY, e -> navigateToMain());
private ColumnsLayout layout = new ColumnsLayout(
firstName, lastName,
company, country,
submit, cancel);

public FormView(CustomerService customerService) {
this.customerService = customerService;
context = BindingContext.of(this, Customer.class, true);
context.onValidate(e -> submit.setEnabled(e.isValid()));
fillCountries();
setColumnsLayout();
self.setMaxWidth(600)
.addClassName("card")
.add(layout);
submit.setStyle("margin-top", "var(--dwc-space-l)");
cancel.setStyle("margin-top", "var(--dwc-space-l)");
}

private void setColumnsLayout() {
List<Breakpoint> breakpoints = List.of(
new Breakpoint(600, 2));
layout.setSpacing("var(--dwc-space-l)")
.setBreakpoints(breakpoints);
}

private void fillCountries() {
ArrayList<ListItem> listCountries = new ArrayList<>();
for (Country countryItem : Customer.Country.values()) {
listCountries.add(new ListItem(countryItem, countryItem.toString()));
}
country.insert(listCountries);
country.selectIndex(0);
}

private void navigateToMain() {
Router.getCurrent().navigate(MainView.class);
}

@Override
public void onWillEnter(WillEnterEvent event, ParametersBag parameters) {
parameters.getInt("id").ifPresentOrElse(id -> {
customerId = Long.valueOf(id);
if (customerService.doesCustomerExist(customerId)) {
event.accept();
fillForm(customerId);
} else {
event.reject();
navigateToMain();
}

}, () -> event.accept());
}

public void fillForm(Long customerId) {
customer = customerService.getCustomerByKey(customerId);
context.read(customer);
}
}

下一步​

下一步,集成应用程序布局,重点使用 AppLayout 添加一个侧边菜单,该菜单对客户表和客户表单页面的用户可用。您还将学习另一个布局工具,FlexLayout 组件。