openxava / 文档 / 第二十六章:参照与集合

课程:1. 入门教学 | 2. 基本域模型(上) | 3. 基本域模型(下) | 4. 优化用户界面 | 5. 敏捷开发 | 6. 映射式超类继承 | 7. 实体继承 | 8. 视图继承(View) | 9. Java 属性 | 10. 计算属性 | 11. 用在集合的 @DefaultValueCalculator | 12. @Calculation 和集合总计 | 13. 从外部文件的 @DefaultValueCalculator | 14. 手动更改 schema | 15. 多用户时默认值的计算 | 16. 同步持久属性和计算属性 | 17. 从数据库中的逻辑 | 18. 使用 @EntityValidator 进行验证 | 19. 验证替代方案 | 20. 删除时验证 | 21. 自定义 Bean Validation 注解 | 22. 在验证中调用 REST 服务 | 23. 注解中的属性 | 24. 改进标准行为 | 25. 行为与业务逻辑 | 26. 参照与集合 | A. Architecture & philosophy | B. Java Persistence API | C. Annotations | D. Automated testing

目录

第 26 章:参照与集合
优化参照行为
验证虽然很好但还不够
优化在列表搜索参照的动作
在输入字段时搜索参照
优化输入键时的搜索
优化集合的行为
优化将元素添加到集合的列表
优化将元素添加到集合的动作
总结
在之前的课程中,您学到如何添加自己的动作。然而,这还不足以完全自定义应用程序的行为,因为生成的用户界面,具体而言是参照和集合的用户界面,具有的标准行为有时并不是最方便的。
幸运的是,OpenXava 提供了许多方法来自定义参照和集合的行为。在本章,您将学到一些自定义的方法,以及它们如何为您的应用程序增加价值。

优化参照行为

您可能已经注意到订单模块有一个小漏洞:用户可以将他想要的任何发票添加到当前订单中,即使发票的客户不同。这是不能接受的,让我们修复它。

验证虽然很好但还不够

当发票和订单都属于同一客户时,用户才能将它们相关联。这是您应用程序的特定业务逻辑,因此标准的 OpenXava 行为无法解决它。下图显示当发票的客户不正确时会产生验证错误:

references-collections_en010

由于这是业务逻辑,我们会把它放在模型层,也就是实体中。我们在此添加验证,这样就可以得到上图的效果。
您已经知道如何将此验证添加到您的 Order 实体了,它是一个用 @AssertTrue 注解的方法:
public class Order {

    ...

    // 此方法必须返回 true 才能使此订单有效
    @AssertTrue(message="customer_order_invoice_must_match") 
    private boolean isInvoiceCustomerMatches() {
    	return invoice == null || // invoice is optional
    		invoice.getCustomer().getNumber() == getCustomer().getNumber();
    }

}
您还必须将消息添加到 src/main/resources/i18n/invoicing-messages_zh.properties:
customer_order_invoice_must_match=发票和订单的客户必须匹配
在这里我们验证发票的客户和这个订单的客户是一样的,以保持数据完整性。但仅此验证是的话,对用户来说会显的有点不够。

优化用于在列表搜索参照的动作

尽管验证可以防止用户为订单分配不正确的发票,但也很难找到正确的发票。因为当用户点击搜索发票时,会显示所有的发票。我们将对此进行改进,以便仅显示当前客户的发票,如下:

references-collections_en020.png


我们将使用 @SearchAction 注解来自定义搜索要参照的发票的动作。以下您可以看到在 Order 类中的修改:
public class Order extends CommercialDocument {

    @ManyToOne
    @ReferenceView("NoCustomerNoOrders")
    @OnChange(ShowHideCreateInvoiceAction.class)
    @SearchAction("Order.searchInvoice") // 定义我们自己的动作来搜索发票
    Invoice invoice;

    ...
	
}
通过这简单的方式,我们定义了当用户点击搜索发票时要执行的动作。 @SearchAction 的参数 Order.searchInvoice 是动作的名称,也就是在 controllers.xml 文件中定义的 Order 控制器的 searchInvoice 动作。现在我们必须编辑 controllers.xml 来添加我们新动作的定义:
<controller name="Order">

    ...
	
    <action name="searchInvoice"
        class="com.yourcompany.invoicing.actions.SearchInvoiceFromOrderAction"
        hidden="true" icon="magnify"/>
        <!--
        hidden="true" : 因为我们不希望动作显示在模块的按钮栏中
        icon="magnify" : 与标准搜索动作相同的图标
        -->
	
</controller>
我们的动作从 ReferenceSearchAction 扩展而来,如下:
package com.yourcompany.invoicing.actions; // 在 actions 包

import org.openxava.actions.*; // 用于使用 ReferenceSearchAction

public class SearchInvoiceFromOrderAction
    extends ReferenceSearchAction { // 搜索参照时的标准逻辑

    public void execute() throws Exception {
        int customerNumber =
            getView().getValueInt("customer.number"); // 从视图中读取当前订单的客户编号
        super.execute(); // 它会执行显示对话框的标准逻辑
        if (customerNumber > 0) { // 如果有客户我们就用它
            getTab().setBaseCondition("${customer.number} = " + customerNumber);
        }
    }
}
可以看到我们如何使用 getTab().setBaseCondition() 为列表选参照而建立条件。也就是说,您可以从 ReferenceSearchAction 使用 getTab() 来操纵列表的行为。
如果没有客户,我们不添加任何条件,因此会显示所有发票,这是当用户在选择客户之前先选择发票的情况。

在输入字段时搜索参照

选择参照的列表已经好了。但是,我们也希望用户可以在不用列表时选择发票,只需输入年份和编号。这在用户知道他想要哪张发票时非常方便。 OpenXava 默认提供此功能。如果 @Id 字段显示在参照中,则会用它们搜索,相反则 OpenXava 会使用第一个字段进行搜索。这在我们的例子中并不方便,因为第一个显示的字段是年份,若仅按年份搜索发票会不准确。下图显示了默认的行为和一个更方便的替代方法:

references-collections_en030.png

幸运的是,我们很容易指出从用户的角度想要使用哪些字段搜索,并通过@SearchKey 注解完成。只需编辑 CommercialDocument 类(记住,它是 Order 和 Invoice 的父类)并将该注解添加到 year 和 number 属性:
abstract public class CommercialDocument extends Deletable {

    @SearchKey // 添加此注解
    @Column(length=4)
    @DefaultValueCalculator(CurrentYearCalculator.class) 
    int year;

    @SearchKey // 添加此注解
    @Column(length=6)
    @ReadOnly
    int number;
	
    ...
	
}
这样,当用户从参照搜索订单或发票时,他必须输入年份和编号,然后相应的实体将从数据库中填充至用户界面。
现在,用户无需使用搜索列表即可轻松选择发票,只需输入年份和编号即可。

优化输入键时的搜索

现在可以使用年份和编号搜索发票,但我们希望对其进行改进,以帮助我们的用户更有效地完成工作。例如,如果用户尚未为订单选择客户并且他选择了发票,则该发票的客户将自动分配给当前订单,这会很方便。在下图可看到我们想要的行为:
references-collections_en040.png

另一方面,如果用户已经为订单选择了客户,但他跟发票的不一样,则会被拒绝并显示错误的消息,像这样:

references-collections_en050.png


为了定义这种特殊行为,我们必须在 Order 的 invoice 参照中添加 @OnChangeSearch 注解。@OnChangeSearch 允许您定义自己的动作以当用户界面中的键更改时搜索。您可以在此处查看修改后的参照:
public class Order extends CommercialDocument {
 
    @ManyToOne
    @ReferenceView("NoCustomerNoOrders") 
    @OnChange(ShowHideCreateInvoiceAction.class)
    @OnChangeSearch(OnChangeSearchInvoiceAction.class) // 添加此注解
    @SearchAction("Order.searchInvoice")
    Invoice invoice;
	
    ...
	
}	
从现在开始,当用户输入发票的新年份和编号时,将执行 OnChangeSearchInvoiceAction 的逻辑。此动作会从数据库中读取发票数据并更新用户界面。以下是动作的代码:
package com.yourcompany.invoicing.actions; // 在 actions 包

import java.util.*;
import org.openxava.actions.*; // 用于使用 OnChangeSearchAction
import org.openxava.model.*;
import org.openxava.view.*;
import com.yourcompany.invoicing.model.*;

public class OnChangeSearchInvoiceAction 
    extends OnChangeSearchAction { // 当搜索参照而用户界面中的键值有变化时的标准逻辑 (1)
    public void execute() throws Exception {
        super.execute(); // 它执行标准逻辑(2)
        Map keyValues = getView()// getView() 这里是参照的视图,不是主视图 (3)
            .getKeyValuesWithValue();
        if (keyValues.isEmpty()) return; // 如果 key 为空,则不执行额外的逻辑
        Invoice invoice = (Invoice) // 我们从输入的键搜索 Invoice 实体 (4)
            MapFacade.findEntity(getView().getModelName(), keyValues);
        View customerView = getView().getRoot().getSubview("customer"); // (5)
        int customerNumber = customerView.getValueInt("number");
        if (customerNumber == 0) { // 如果没有客户,我们填 (6)
            customerView.setValue("number", invoice.getCustomer().getNumber());
            customerView.refresh();
        } 
        else { // 如果已经有客户,我们验证他是否跟发票的客户匹配 (7)
            if (customerNumber != invoice.getCustomer().getNumber()) {
                addError("invoice_customer_not_match", 
                    invoice.getCustomer().getNumber(), invoice, customerNumber);
                getView().clear();
            }
        }
    }
}	
鉴于该动作从 OnChangeSearchAction (1) 扩展而来,并且我们使用 super.execute() (2),它的行为只是标准方式,也就是当用户输入年份和编号时,发票的数据会填充侧用户界面。之后,我们使用 getView() (3) 获取所显示发票的 key 以使用 MapFacade (4) 找到对应的实体。从 OnChangeSearchAction getView() 内部返回是参照的子视图,而不是全局视图。因此,在这种情况下 getView() 是参照发票的视图。这允许您创建更多可重用的 @OnChangeSearch 动作。因此,您必须编写 getView().getRoot().getSubview("customer") (5) 才能访问客户视图。
为了实现上图的行为,动作会询问是否没有客户 (customberNumber == 0) (6)。如果是这种情况,它会从发票的客户中填写客户。否则,它会执行上图中的逻辑,以验证当前订单的客户是否与发票的客户匹配。
最后的细节是消息。将以下条目添加到 src/main/resources/i18n 文件夹中的 invoicing-messages_zh.properties 文件。
invoice_customer_not_match=发票 {1} 的客户编号 {0} 与当前订单的客户编号 {2} 不匹配
@OnChangeSearch 的有趣之处在于,当从列表中选发票时也会执行,因为在这种情况下,发票的年份和编号也会发生变化。因此,这是一个优化参照并填充视图的逻辑。

优化集合的行为

我们可以像优化参照一样优化集合。这非常有用,因为它允许我们改进 Invoice 模块当前的行为。如果发票和订单属于同一客户,用户才能将订单添加到发票中。此外,订单必须已送达,并且必须没有发票。

优化将元素添加到集合的列表

当前,当用户想将订单添加到发票时,所有订单都可选取。我们将对此进行改进,以仅显示该发票的客户的订单,并且已送达而尚未开发票,如下图所示:
references-collections_en60.png
我们将使用@AddAction 注解来定义我们自己的动作以显示添加订单的列表。以下代码是 Invoice 类中所需的修改。
public class Invoice extends CommercialDocument {

    @OneToMany(mappedBy="invoice")
    @CollectionView("NoCustomerNoInvoice")
    @AddAction("Invoice.addOrders") // 定义我们的添加订单的动作
    Collection<Order> orders;

    ...
	
}
通过这简单的方式,我们定义了当用户点击添加订单时要执行的动作。 @AddAction 的参数 Invoice.addOrders 是动作的名称,也就是在 controllers.xml 文件中 Invoice 控制器定义的 addOrders 动作。
现在我们必须编辑 controllers.xml 以添加 Invoice 控制器和新动作:
<controller name="Invoice">
    <extends controller="Invoicing"/>

    <action name="addOrders"
        class="com.yourcompany.invoicing.actions.GoAddOrdersToInvoiceAction"
        hidden="true" icon="table-row-plus-after"/>
        <!--
        hidden="true" : 因为我们不希望动作显示在模块的按钮栏中
        icon="table-row-plus-after" : 与标准動作相同的图标
        -->

</controller>
这是动作的代码:
package com.yourcompany.invoicing.actions; // 在 actions 包

import org.openxava.actions.*; // 用于使用 GoAddElementsToCollectionAction

public class GoAddOrdersToInvoiceAction
    extends GoAddElementsToCollectionAction { // 到列表的标准逻辑(将元素添加到集合的列表)
    public void execute() throws Exception {
        super.execute(); // 它执行标准逻辑,显示一个对话框
        int customerNumber =
            getPreviousView() // getPreviousView() 是主视图(我们在一个对话框中)
                .getValueInt("customer.number"); // 读取当前发票视图的客户编号
        getTab().setBaseCondition( // 要添加的订单列表的条件
            "${customer.number} = " + customerNumber +
            " and ${delivered} = true and ${invoice} is null"
        );
    }
}
请看我们如何使用 getTab().setBaseCondition() 在列表建立条件以选择要添加的实体。也就是说,您可以从 GoAddElementsToCollectionAction 使用 getTab() 来操作列表的行为方式。

优化将元素添加到集合的动作

对订单集合一个优化是,当用户将订单添加到当前发票时,这些订单的详情会自动复制到发票中。
我们在此无法使用 @AddAction,因为它用于显示添加至集合的元素列表。但并不是添加元素的动作。
在这节我们将学到如何定义添加元素的动作:
references-collections_en70.png
不幸的是,没有直接定义此“添加”动作的注解。然而,这并不是很难的任务,我们只需要改进 @AddAction,让它显示我们想要的控制器,并在这个控制器中放置我们想要的动作。由于我们已经在上一节中定义了 @AddAction,我们只需在已存在的 GoAddOrdersToInvoiceAction 类添加一个新方法。将以下 getNextController() 方法添加到您的动作中:
public class GoAddOrdersToInvoiceAction ... {

    ...

    public String getNextController() { // 添加这个方法
        return "AddOrdersToInvoice"; // 具有动作的控制器(添加订单列表中的动作)
    }                                
}
默认情况下,我们在实体列表中要添加的动作(添加和取消按钮)来自 OpenXava 标准控制器 AddToCollection。我们在动作中覆盖 getNextController() 以允许我们定义自己的控制器。请在 controllers.xml 中定义我们的控制器:
<controller name="AddOrdersToInvoice">
    <extends controller="AddToCollection" /> <!-- 从标准控制器扩展 -->
	
    <!-- 覆盖 add 的动作 -->
    <action name="add"
        class="com.yourcompany.invoicing.actions.AddOrdersToInvoiceAction" />
		
</controller>
这样,将订单添加到发票的动作是 AddOrdersToInvoiceAction。请记住,我们动作的目标是以通用的方式将订单添加到发票中,并且还将这些订单的详情复制到发票中。以下是动作的代码:
package com.yourcompany.invoicing.actions; // 在 actions 包中
import java.rmi.*;
import java.util.*;
import javax.ejb.*;
import org.openxava.actions.*; // 用于使用 AddElementsToCollectionAction
import org.openxava.model.*;
import org.openxava.util.*;
import org.openxava.validators.*;
import com.yourcompany.invoicing.model.*;

public class AddOrdersToInvoiceAction
    extends AddElementsToCollectionAction { // 将元素添加集合的标准逻辑
    public void execute() throws Exception {
        super.execute(); // 我们如实使用标准逻辑
        getView().refresh(); // 用于显示新数据,包括重新计算的税额等
    }                       

    protected void associateEntity(Map keyValues) // 用於关联的方法,在本例中将每个订单关联到发票
        throws ValidationException, 
            XavaException, ObjectNotFoundException,
            FinderException, RemoteException
    {
        super.associateEntity(keyValues); //它执行标准逻辑 (1)
        Order order = (Order) MapFacade.findEntity("Order", keyValues); // (2)
        order.copyDetailsToInvoice(); // 将主要工作委托给实体 (3)
    }
}
我们覆盖 execute() 方法只是为了在执行后刷新视图。我们想要的是优化订单与发票关联的逻辑。为此我们得覆盖 associateEntity() 方法。而这里的逻辑很简单,在执行标准逻辑(1)之后,我们搜索对应的 Order 实体,然后为该Order 调用 copyDetailsToInvoice()。幸运的是,我们已经有了将订单中的详情复制到发票的方法,我们只需调用此方法即可。
现在您只需新建发票、选择客户并添加订单。它甚至比使用订单模块的列表模式更容易,因为在发票模块中只会显示该客户的订单。

总结

本章向您展示如何优化参照和集合的标准行为,让应用程序满足用户的需求。在此您只看到一些示例,但 OpenXava 为集合和参照提供了更多优化的可能性,例如使用以下的注解:@ReferenceView、@ReadOnly、@NoFrame、@NoCreate、@NoModify、@NoSearch、@AsEmbedded、@SearchAction、@DescriptionsList、@LabelFormat、@Action、@OnChange、@OnChangeSearch、@Editor、@CollectionView、@EditOnly、@ListProperties、@RowStyle、@EditAction、@ViewAction、@NewAction、@SaveAction、@HideDetailAction、@RemoveAction、@RemoveSelectedAction、@ListAction、@DetailAction 和 @OnSelectElementAction。请查看参照自定义集合自定义的指南。
如果这还不够,您可以选择为参照或集合定义专属编辑器 。编辑器允许您创建自定义的用户界面组件以显示和编辑参照或集合。
这种灵活性允许您在面对现实生活中的业务应用程序时能自动生成大多所需的用户界面。

下载本课源代码

对这节课有什么问题吗? 前往论譠