blob: bba463597623a850afd3c2b38372a5b7abcf5e51 [file] [log] [blame]
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
//
= NetBeans 平台 CRUD 应用程序教程
:jbake-type: platform_tutorial
:jbake-tags: tutorials
:jbake-status: published
:syntax: true
:source-highlighter: pygments
:toc: left
:toc-title:
:icons: font
:experimental:
:description: NetBeans 平台 CRUD 应用程序教程 - Apache NetBeans
:keywords: Apache NetBeans Platform, Platform Tutorials, NetBeans 平台 CRUD 应用程序教程
本教程介绍了如何将 Java DB 数据库集成到 NetBeans 平台应用程序中。我们先从分析 Java DB 数据库入手,将通过这些数据库创建实体类。但请注意,这些说明并非仅适用于 Java DB。对于 NetBeans IDE 所支持的任何关系数据库,这些说明均适用。接下来,我们将把这些实体类和相关 JPA JAR 的模块一起包装到一个模块中。
在以上模块成为应用程序的一部分之后,我们将创建一个为应用程序提供用户界面的新模块。该新模块将为用户提供一个显示数据库中数据的树状分层结构。然后,我们将创建另一个模块,以使用户能够编辑第一个模块显示的数据。我们将使用不同模块将查看器和编辑器分开,以使用户能够为同一查看器安装不同的编辑器,因为外部供应商会创建各种不同的编辑器,有些用于商业用途,有些则是免费提供的。正是 NetBeans 平台的模块化体系结构促成了这种灵活性。
有了编辑器后,我们将开始添加 CRUD 功能。首先,上面所述的查看器将处理代表“读取”的 "R"。接下来,将处理代表“更新”的 "U",然后依次是代表“创建”的 "C" 和代表“删除”的 "D"
在本教程结束时,您将了解到多种帮助您创建此类应用程序的 NetBeans 平台功能。例如,您将会了解到 `` link:http://bits.netbeans.org/dev/javadoc/org-openide-awt/org/openide/awt/UndoRedo.Manager.html[UndoRedo.Manager]`` 和 `` link:http://bits.netbeans.org/dev/javadoc/org-openide-explorer/org/openide/explorer/ExplorerManager.html[ExplorerManager]`` ,以及一些 NetBeans 平台 Swing 组件,如 `` link:http://bits.netbeans.org/dev/javadoc/org-openide-windows/org/openide/windows/TopComponent.html[TopComponent]`` 和 `` link:http://bits.netbeans.org/dev/javadoc/org-openide-explorer/org/openide/explorer/view/BeanTreeView.html[BeanTreeView]`` 。
*注意:*本文档使用的是 NetBeans IDE 6.8 发行版。如果使用的是早期版本,请参见 link:67/nbm-crud.html[本文档的 6.7 版]。
在本教程中创建的应用程序将如下所示:
image::http://netbeans.dzone.com/sites/all/files/customer-app-on-nb.png[]
建议您在开始学习本教程之前,先观看截屏视频系列 link:https://netbeans.apache.org/tutorials/nbm-10-top-apis.html[Top 10 NetBeans APIs](最主要的 10 个 NetBeans API)。本教程中提到的许多概念都在该截屏视频系列中进行了详细讨论。
== 设置应用程序
首先让我们来创建一个新的 NetBeans 平台应用程序。
[start=1]
1. 选择“文件”>“新建项目”(Ctrl+Shift+N)。在“类别”下选择“NetBeans 模块”。在“项目”下选择“NetBeans 平台应用程序”。单击“下一步”。
[start=2]
1. 在“名称和位置”面板的“项目名称”字段中键入 ``DBManager`` 。单击“完成”。
IDE 将创建 ``DBManager`` 项目。此项目是一个容器,可包含您将创建的所有其他模块。
image::images/crud_68dbmanager-1.png[]
== 集成数据库
为了集成数据库,您需要从数据库创建实体类,并将这些实体类及其相关的 JAR 一起集成到 NetBeans 平台应用程序所包含的模块中。
=== 创建实体类
在此部分,您将从一个选定的数据库生成实体类。
[start=1]
1. 就本示例而言,将使用“服务”窗口连接到 NetBeans IDE 附带的样例数据库:
image::images/crud_68dbmanager-2.png[]
或者,您可以随意使用任何数据库,并根据特定的用例调整操作步骤。对于 MySQL,请参见 link:https://netbeans.apache.org/kb/docs/ide/mysql_zh_CN.html[连接 MySQL 数据库]。
[start=2]
1. IDE 中,选择“文件”|“新建项目”,然后选择 "Java" |“Java 类库”以创建一个名为 ``CustomerLibrary`` 的新库项目。
[start=3]
1. 在“项目”窗口中,右键单击该库项目,选择“文件”|“新建文件”,然后从“数据库”中选择“持久性”|“实体类”。在向导中,选择数据库和所需的表。此处,我们选择 "Customer",将会自动添加 "Discount Code",因为这两个表是关联的。
image::images/crud_68dbmanager-3.png[]
[start=4]
1. 指定持久性策略,它可以是任何可用的选项。此处,由于我们需要选择一些内容,因此,我们选择了 EclipseLink
image::images/crud_68dbmanager-4.png[]
[start=5]
1. "demo" 指定为将在其中生成实体类的包的名称。
image::images/crud_68dbmanager-5.png[]
[start=6]
1. 单击“完成”。完成此步骤后,查看生成的代码,注意,在一个名为 META-INF 的文件夹中出现了一个 ``persistence.xml`` 文件,并且每个表都有了实体类,等等:
image::images/crud_68dbmanager-7.png[]
[start=7]
1. 生成 Java 库,在库项目的 "dist" 文件夹中将出现一个 JAR 文件,可在“文件”窗口中查看此文件。
image::images/crud_68dbmanager-8.png[]
=== 将实体类 JAR 包装到模块中
在本节中,将在应用程序中添加第一个模块!新 NetBeans 模块将包装在上一节中创建的 JAR 文件。
[start=1]
1. 在“项目”窗口中右键单击 ``DBManager`` 的“模块”节点,然后选择“添加新库”。
[start=2]
1. 选择上一子部分创建的 JAR 并完成向导,指定任意值。假设应用程序用于处理 shop.org 上的客户,在这种情况下,唯一标识符 "org.shop.model" 对应于代码名称基:
image::images/crud_68dbmanager-9.png[]
现在,新应用程序中已包含第一个定制模块,它包装的 JAR 包含实体类和 persistence.xml 文件:
image::images/crud_68dbmanager-91.png[]
=== 创建其他相关模块
在此部分,将创建两个新模块,用来包装 EclipseLink JAR 和数据库连接器 JAR
[start=1]
1. 执行与为实体类 JAR 创建库包装器相同的操作,但现在使用的是 EclipseLink JAR,它位于先前创建的 "CustomerLibrary" Java 库中:
image::images/crud_68dbmanager-94.png[]
在“库包装模块”向导中,您可以按住 Ctrl 键并单击以选择多个 JAR
[start=2]
1. 接下来,再创建一个库包装模块,这次是为 Java DB 客户端 JAR 创建的,它位于 JDK 分发的 ``db/lib/derbyclient.jar`` 中。
=== 设计用户界面
在此部分,将创建一个简单的原型用户界面,此界面提供了一个使用 ``JTextArea`` 显示从数据库检索的数据的窗口。
[start=1]
1. 在“项目”窗口中右键单击 ``DBManager`` 的“模块”节点,然后选择“添加新模块”。创建一个名为 ``CustomerViewer`` 的新模块,其代码名称基为 ``org.shop.ui``
[start=2]
1. 在“项目”窗口中,右键单击该新模块,然后选择“新建”|“窗口组件”。指定应在 ``editor`` 位置创建该窗口组件,并且在应用程序启动时应将其打开。将 ``Customer`` 设置为该窗口的类名前缀。
[start=3]
1. 使用“组件面板”(Ctrl-Shift-8) 将一个 ``JTextArea`` 拖放到新窗口中。
image::images/crud_68dbmanager-93.png[]
[start=4]
1. TopComponent 构造函数的末尾添加以下代码:
[source,java]
----
EntityManager entityManager = Persistence.createEntityManagerFactory("CustomerLibraryPU").createEntityManager();
Query query = entityManager.createQuery("SELECT c FROM Customer c");
List<Customer> resultList = query.getResultList();
for (Customer c : resultList) {
jTextArea1.append(c.getName() + " (" + c.getCity() + ")" + "\n");
}
----
因为您未在提供 Customer 对象和持久性 JAR 的模块上设置依赖关系,将使用表示错误的红色下划线标记上面的语句。此问题将在下一部分中解决。
在上面,您可以看到对一个名为 "CustomerLibraryPU" 的持久性单元的引用,此名称是在 ``persistence.xml`` 文件中设置的。此外,还有一个对名为 ``Customer`` 的实体类的引用,该实体类位于实体类模块中。如果不同于上面的内容,请根据需要修改这些代码。
=== 设置依赖关系
在此部分,将使一些模块能够使用其他模块中的代码。通过在相关模块之间设置约定来清楚地执行此操作,即不会随意重复滥用代码(在没有诸如 NetBeans 平台所提供的严格模块化体系结构时容易发生此情况)。
[start=1]
1. 实体类模块需要依赖于 Derby 客户端模块以及 EclipseLink 模块。右键单击 ``CustomerLibrary`` 模块,选择“属性”,然后使用“库”标签设置对 ``CustomerLibrary`` 模块所需的两个模块的依赖关系。
[start=2]
1. ``CustomerViewer`` 模块需要依赖于 EclipseLink 模块以及实体类模块。右键单击 ``CustomerViewer`` 模块,选择“属性”,然后使用“库”标签设置对 ``CustomerViewer`` 模块所需的两个模块的依赖关系。
[start=3]
1. 在“源”视图中打开 ``CustomerTopComponent`` ,在编辑器中单击鼠标右键,然后选择“修复导入”。IDE 现在可以添加所需的导入语句,因为提供所需类的模块现已可用于 ``CustomerTopComponent``
现在,您已在应用程序中的各模块之间设置了约定,从而可以控制不同代码段之间的依赖关系。
=== 运行原型
在此部分,将运行该应用程序,以便查看能否正确访问数据库。
[start=1]
1. 启动数据库服务器。
[start=2]
1. 运行应用程序。您应看到如下所示的内容:
image::images/crud_68dbmanager-92.png[]
现在,您已具有一个简单原型,它包含的 NetBeans 平台应用程序将显示数据库中的数据,下一节将对其进行扩展。
== 集成 CRUD 功能
为了创建与 NetBeans 平台顺利集成的 CRUD 功能,需要实现一些特定的 NetBeans 平台编码模式。以下部分详细介绍了这些模式。
=== 读取
在此部分,将针对 NetBeans 平台资源管理器视图更改上一部分中引入的 ``JTextArea`` NetBeans 平台资源管理器视图是一种 Swing 组件,与标准 Swing 组件相比,此组件与 NetBeans 平台集成的效果更好。它们支持很多功能,其中之一是上下文概念,以便与上下文相关联。
为了表示数据,NetBeans 平台 ``Node`` 类将提供一个通用的分层结构模型,此模型可通过任何 NetBeans 平台资源管理器视图显示。本部分末尾说明了如何将资源管理器视图与 NetBeans 平台“属性”窗口进行同步。
[start=1]
1. 对于 ``TopComponent`` ,在“设计”视图中删除 ``JTextArea`` ,并在“源”视图中注释掉其相关代码:
[source,java]
----
EntityManager entityManager = Persistence.createEntityManagerFactory("CustomerLibraryPU").createEntityManager();
Query query = entityManager.createQuery("SELECT c FROM Customer c");
List<Customer> resultList = query.getResultList();
//for (Customer c : resultList) {
// jTextArea1.append(c.getName() + " (" + c.getCity() + ")" + "\n");
//}
----
[start=2]
1. 右键单击 ``CustomerViewer`` 模块,选择“属性”,然后使用“库”标签设置对节点 API 以及资源管理器和属性表单 API 的依赖关系。
[start=3]
1. 接下来,更改类签名以实现 ``ExplorerManager.Provider``
[source,java]
----
final class CustomerTopComponent extends TopComponent implements ExplorerManager.Provider
----
将需要覆盖 ``getExplorerManager()``
[source,java]
----
@Override
public ExplorerManager getExplorerManager() {
return em;
}
----
在类的顶部声明并初始化 ``ExplorerManager``
[source,java]
----
private static ExplorerManager em = new ExplorerManager();
----
有关以上代码的详细信息,请观看 link:https://netbeans.apache.org/tutorials/nbm-10-top-apis.html[Top 10 NetBeans APIs](最主要的 10 个 NetBeans API),特别是处理节点 API 以及资源管理器和属性表单 API 的截屏视频。
[start=4]
1. 切换到 ``TopComponent`` 的“设计”视图,在“组件面板”中单击鼠标右键,选择“组件面板管理器”|“从 JAR 添加”。然后找到 NetBeans IDE 安装目录下 ``platform11/modules`` 文件夹中的 ``org-openide-explorer.jar`` 。选择 BeanTreeView,然后完成向导。现在,应在“组件面板”中看到 ``BeanTreeView`` 。将其从“组件面板”拖放到窗口上。
[start=5]
1. 创建一个工厂类,它将为数据库中的每个客户创建一个新的 link:http://bits.netbeans.org/dev/javadoc/org-netbeans-modules-db/org/netbeans/api/db/explorer/node/BaseNode.html[BeanNode]:
[source,java]
----
import demo.Customer;
import java.beans.IntrospectionException;
import java.util.List;
import org.openide.nodes.BeanNode;
import org.openide.nodes.ChildFactory;
import org.openide.nodes.Node;
import org.openide.util.Exceptions;
public class CustomerChildFactory extends ChildFactory<Customer> {
private List<Customer> resultList;
public CustomerChildFactory(List<Customer> resultList) {
this.resultList = resultList;
}
@Override
protected boolean createKeys(List<Customer> list) {
for (Customer Customer : resultList) {
list.add(Customer);
}
return true;
}
@Override
protected Node createNodeForKey(Customer c) {
try {
return new BeanNode(c);
} catch (IntrospectionException ex) {
Exceptions.printStackTrace(ex);
return null;
}
}
}
----
[start=6]
1. 返回到 ``CustomerTopComponent`` ,使用 ``ExplorerManager`` 将来自 JPA 查询的结果列表传递到 ``Node``
[source,java]
----
EntityManager entityManager = Persistence.createEntityManagerFactory("CustomerLibraryPU").createEntityManager();
Query query = entityManager.createQuery("SELECT c FROM Customer c");
List<Customer> resultList = query.getResultList();
*em.setRootContext(new AbstractNode(Children.create(new CustomerChildFactory(resultList), true)));*
//for (Customer c : resultList) {
// jTextArea1.append(c.getName() + " (" + c.getCity() + ")" + "\n");
//}
----
[start=7]
1. 运行应用程序。在应用程序运行后,打开“属性”窗口。请注意,尽管数据可用并显示在 ``BeanTreeView`` 中,但 ``BeanTreeView`` 不会与“属性”窗口(可通过选择“窗口”|“属性”打开该窗口)保持同步。换言之,在树状分层结构中上下移动时,“属性”窗口中不会显示任何内容。
[start=8]
1. 通过向 ``TopComponent`` 中的构造函数添加以下代码,将“属性”窗口与 ``BeanTreeView`` 进行同步。
[source,java]
----
associateLookup(ExplorerUtils.createLookup(em, getActionMap()));
----
这里我们将 ``TopComponent`` ``ActionMap`` ``ExplorerManager`` 添加到 ``TopComponent`` ``Lookup`` 中。此操作的一个副效应是“属性”窗口开始显示选定 ``Node`` 的显示名称和工具提示文本。
[start=9]
1. 再次运行应用程序,注意,“属性”窗口现在与资源管理器视图保持同步:
image::images/crud_68dbmanager-95.png[]
现在,您可以在树状层次结构中查看数据,就如同使用 ``JTree`` 一样。但是,您还可以切换到其他浏览器视图,而无需更改该模型,因为 ``ExplorerManager`` 会在模型和视图之间进行协调。最后,您还可以将视图与“属性”窗口进行同步。
=== 更新
在此部分,将首先创建一个编辑器。该编辑器将由一个新的 NetBeans 模块提供。因此,首先需要创建一个新的模块。然后,在新模块中创建一个新的 ``TopComponent`` ,其中含有两个 ``JTextField`` (分别用于允许用户编辑的两个列)。您将需要使查看器模块与编辑器模块进行通信。每当在查看器模块中选择新的 ``Node`` 时,都会将当前的 ``Customer`` 对象添加到 ``Lookup`` 中。在编辑器模块中,将需要侦听 ``Lookup`` 以确定是否引入了 ``Customer`` 对象。每当将新的 ``Customer`` 对象引入到 ``Lookup`` 时,都会在编辑器中更新 ``JTextField``
接下来,将 ``JTextField`` NetBeans 平台的撤销、重做和保存功能进行同步。换言之,当用户更改 ``JTextField`` 时,您希望可以使用 NetBeans 平台的现有功能,以便无需创建新功能,即可轻松获得 NetBeans 平台支持。为此,您需要使用 ``UndoRedoManager`` ``SaveCookie``
[start=1]
1. 创建一个新模块,命名为 ``CustomerEditor`` ,并将 ``org.shop.editor`` 作为其代码名称基。
[start=2]
1. 右键单击 ``CustomerEditor`` 模块,然后选择“新建”|“窗口组件”。确保指定在 ``editor`` 位置显示该窗口,并在应用程序启动时将其打开。在向导的最后一个面板中,将 "Editor" 设置为类名称前缀。
[start=3]
1. 使用“组件面板”(Ctrl-Shift-8) 向新窗口中添加两个 ``JLabel`` 和两个 ``JTextField`` 。将标签的文本设置为 "Name" "City",并将两个 ``JTextField`` 的变量名称设置为 ``jTextField1`` ``jTextField2``
GUI 生成器中,该窗口的外观应与下图类似:
image::images/crud_68dbmanager-96.png[]
[start=4]
1. 返回到 ``CustomerViewer`` 模块并更改其 ``layer.xml`` 文件,指定 ``CustomerTopComponent`` 窗口将以 ``explorer`` 模式显示。
在更改 ``layer.xml`` 文件后,右键单击该应用程序项目,然后选择“清理”。为什么要这样做?因为每当运行应用程序并将其关闭后,都会将窗口位置存储到用户目录中。因此,如果 ``CustomerViewer`` 最初以 ``editor`` 模式显示,则会一直处于 ``editor`` 模式,直到执行“清理”操作,此操作会重置用户目录(即,删除用户目录),并使 ``CustomerViewer`` ``layer.xml`` 文件中当前设置的位置显示。
同时,检查当用户调整应用程序大小时, ``CustomerViewer`` 中的 ``BeanTreeView`` 能否水平或垂直拉伸。检查方法是打开窗口,选择 ``BeanTreeView`` ,然后单击 GUI 生成器工具栏中的箭头按钮。
[start=5]
1. 运行应用程序,并确保在应用程序启动时显示以下内容:
image::images/crud_68dbmanager-97.png[]
[start=6]
1. 现在我们可以开始添加一些代码。第一,我们需要在编辑器中显示当前选中的 Customer 对象:
* 首先调整 ``CustomerViewer`` 模块,以便每当选择了新的 ``Node`` 时,都会将当前的 ``Customer`` 对象添加到查看器窗口的 ``Lookup`` 中。为此,请在 ``CustomerChildFactory`` 类中创建 ``AbstractNode`` ,而不是 ``BeanNode`` 。这样,您就可以将当前 ``Customer`` 对象添加到该 Node ``Lookup`` 中,如下所示(请注意粗体部分):
[source,java]
----
@Override
protected Node createNodeForKey(Customer c) {
Node node = new AbstractNode(Children.LEAF, Lookups.singleton(c));
node.setDisplayName(c.getName());
node.setShortDescription(c.getCity());
return node;
// try {
// return new BeanNode(c);
// } catch (IntrospectionException ex) {
// Exceptions.printStackTrace(ex);
// return null;
// }
}
----
现在,每当创建新的 ``Node`` (当用户在查看器中选择新的客户时即会执行此操作)时,就会将新的 ``Customer`` 对象添加到该 ``Node`` ``Lookup`` 中。
* 现在,我们将通过某种方式来更改编辑器模块,使其窗口最终侦听被添加到 ``Lookup`` ``Customer`` 对象。首先,在编辑器模块中设置对提供实体类的模块以及提供持久性 JAR 的模块的依赖关系。
* 接下来,更改 ``EditorTopComponent`` 类签名以实现 ``LookupListner``
[source,java]
----
public final class EditorTopComponent extends TopComponent implements LookupListener
----
* 覆盖 ``resultChanged`` ,以便在将新的 ``Customer`` 对象引入 ``Lookup`` 中时,对 ``JTextField`` 进行更新:
[source,java]
----
@Override
public void resultChanged(LookupEvent lookupEvent) {
Lookup.Result r = (Lookup.Result) lookupEvent.getSource();
Collection<Customer> coll = r.allInstances();
if (!coll.isEmpty()) {
for (Customer cust : coll) {
jTextField1.setText(cust.getName());
jTextField2.setText(cust.getCity());
}
} else {
jTextField1.setText("[no name]");
jTextField2.setText("[no city]");
}
}
----
* 现在定义了 ``LookupListener`` ,我们需要将其添加到某个对象。这里,我们将其添加到从全局上下文中获取的 ``Lookup.Result`` 中。全局上下文将代理选定 ``Node`` 的上下文。例如,如果在树状分层结构中选择了 "Ford Motor Co",则会将 "Ford Motor Co" ``Customer`` 对象添加到该 ``Node`` ``Lookup`` 中,这意味着 "Ford Motor Co" ``Customer`` 对象当前在全局上下文中可用(因为该节点为当前选定的 ``Node`` )。随后即会将此对象传递到 ``resultChanged`` ,以填充该文本字段。
每当编辑器窗口打开时,便开始执行上述所有操作,即会激活 ``LookupListener`` ,如下所示:
[source,java]
----
@Override
public void componentOpened() {
result = Utilities.actionsGlobalContext().lookupResult(Customer.class);
result.addLookupListener(this);
resultChanged(new LookupEvent(result));
}
@Override
public void componentClosed() {
result.removeLookupListener(this);
result = null;
}
----
由于编辑器窗口会在应用程序启动时打开,因此在应用程序启动时 ``LookupListener`` 即可用。
* 最后,在类的顶部声明结果变量,如下所示:
[source,java]
----
private Lookup.Result result = null;
----
* 再次运行应用程序,注意,每当选择一个新的 ``Node`` 时,编辑器窗口即会更新。
image::images/crud_68dbmanager-98.png[]
但请注意,将焦点切换到编辑器窗口时会发生什么情况:
image::images/crud_68dbmanager-99.png[]
由于该 ``Node`` 不再是当前节点,因此 ``Customer`` 对象不再位于全局上下文中。这是因为全局上下文代理的是当前 ``Node`` ``Lookup`` (如上文中所述)。因此,在这种情况下,我们不能使用全局上下文。而应使用 Customer 窗口提供的本地 ``Lookup``
将此行:
[source,java]
----
result = Utilities.actionsGlobalContext().lookupResult(Customer.class);
----
重写为:
[source,java]
----
result = WindowManager.getDefault().findTopComponent("CustomerTopComponent").getLookup().lookupResult(Customer.class);
----
字符串 "CustomerTopComponent" ``CustomerTopComponent`` ID,它是一个字符串常量,可以在 ``CustomerTopComponent`` 源代码中找到。上述方法有一个缺点,即, ``EditorTopComponent`` 仅在找到 ID "CustomerTopComponent" ``TopComponent`` 时才发挥作用。可通过以下两种方法解决此问题:明确记录此问题,以使其他编辑器的开发人员了解他们需要这样标识查看器 ``TopComponent`` ,或者重写该选定模型,如 Tim Boudreau link:http://weblogs.java.net/blog/timboudreau/archive/2007/01/how_to_replace.html[在此处所述]。
如果使用上述方法之一,您会发现将焦点切换到 ``EditorTopComponent`` 时,上下文并未丢失,如下所示:
image::images/crud_68dbmanager-991.png[]
由于您现在使用的是 ``AbstractNode`` 而不是 ``BeanNode`` ,“属性”窗口中不会显示任何属性。您需要自行提供这些属性,如 link:https://netbeans.apache.org/tutorials/nbm-nodesapi2.html[节点 API 教程]中所述。
[start=7]
1. 然后,让我们来处理撤销/重做功能。当用户更改某个 ``JTextField`` 时,我们希望启用“撤销”按钮、“重做”按钮以及“编辑”菜单中的相关菜单项。为此,NetBeans 平台提供了 link:http://bits.netbeans.org/dev/javadoc/org-openide-awt/org/openide/awt/UndoRedo.Manager.html[UndoRedo.Manager]。
* ``EditorTopComponent`` 项部声明并实例化一个新的 UndoRedoManager
[source,java]
----
private UndoRedo.Manager manager = new UndoRedo.Manager();
----
* 接下来,覆盖 ``EditorTopComponent`` 中的 ``getUndoRedo()`` 方法:
[source,java]
----
@Override
public UndoRedo getUndoRedo() {
return manager;
}
----
* ``EditorTopComponent`` 的构造函数中,向 ``JTextField`` 中添加一个 ``KeyListener`` ,并在需要实现的相关方法中,添加 ``UndoRedoListener``
[source,java]
----
jTextField1.getDocument().addUndoableEditListener(manager);
jTextField2.getDocument().addUndoableEditListener(manager);
----
* 运行应用程序并显示运行中的撤销和重做功能,即相关按钮和菜单项。功能将按预期方式运行。您可能需要更改 ``KeyListener`` ,以免任何键都可启用撤销/重做功能。例如,当按下 Enter 键时,您可能不希望启用撤销/重做功能。因此,请调整上述代码以满足您的业务需求。
[start=8]
1. 接下来,我们需要集成 NetBeans 平台的保存功能。
* 缺省情况下,NetBeans 平台工具栏中提供了“全部保存”按钮。在当前情况下,我们并不希望保存“全部”,因为“全部”指许多不同的文档。在本例中,只有一个“文档”,即供树状分层结构中所有节点重复使用的编辑器。删除“全部保存”按钮,然后添加“保存”按钮,方法是向 ``CustomerEditor`` 模块的层文件中添加以下代码:
[source,xml]
----
<folder name="Toolbars">
<folder name="File">
<file name="org-openide-actions-SaveAction.shadow">
<attr name="originalFile" stringvalue="Actions/System/org-openide-actions-SaveAction.instance"/>
<attr name="position" intvalue="444"/>
</file>
<file name="org-openide-actions-SaveAllAction.shadow_hidden"/>
</folder>
</folder>
----
现在运行应用程序时,将在工具栏中看到一个不同的图标。现在我们可以使用“保存”按钮,而非“全部保存”按钮。
* 设置对于对话框 API 和节点 API 的依赖关系。
* ``EditorTopComponent`` 构造函数中添加一个调用,以便在每次检测到更改时触发一个方法(将在下一步骤中定义):
[source,java]
----
public EditorTopComponent() {
...
...
...
jTextField1.getDocument().addDocumentListener(new DocumentListener() {
public void insertUpdate(DocumentEvent arg0) {
fire(true);
}
public void removeUpdate(DocumentEvent arg0) {
fire(true);
}
public void changedUpdate(DocumentEvent arg0) {
fire(true);
}
});
jTextField2.getDocument().addDocumentListener(new DocumentListener() {
public void insertUpdate(DocumentEvent arg0) {
fire(true);
}
public void removeUpdate(DocumentEvent arg0) {
fire(true);
}
public void changedUpdate(DocumentEvent arg0) {
fire(true);
}
});
//Create a new instance of our SaveCookie implementation:
impl = new SaveCookieImpl();
//Create a new instance of our dynamic object:
content = new InstanceContent();
//Add the dynamic object to the TopComponent Lookup:
associateLookup(new AbstractLookup(content));
}
...
...
...
----
* 以下是上面提到的两种方法。首先,每当检测到更改,就会触发该方法。每次检测到更改时,就会将节点 API 中的 ``SaveCookie`` 实现添加到 ``InstanceContent`` 中:
[source,java]
----
public void fire(boolean modified) {
if (modified) {
//If the text is modified,
//we add SaveCookie impl to Lookup:
content.add(impl);
} else {
//Otherwise, we remove the SaveCookie impl from the lookup:
content.remove(impl);
}
}
private class SaveCookieImpl implements SaveCookie {
@Override
public void save() throws IOException {
Confirmation message = new NotifyDescriptor.Confirmation("Do you want to save \""
+ jTextField1.getText() + " (" + jTextField2.getText() + ")\"?",
NotifyDescriptor.OK_CANCEL_OPTION,
NotifyDescriptor.QUESTION_MESSAGE);
Object result = DialogDisplayer.getDefault().notify(message);
//When user clicks "Yes", indicating they really want to save,
//we need to disable the Save action,
//so that it will only be usable when the next change is made
//to the JTextArea:
if (NotifyDescriptor.YES_OPTION.equals(result)) {
fire(false);
//Implement your save functionality here.
}
}
}
----
* 运行应用程序并注意“保存”按钮的启用/禁用情况。
image::images/crud_68dbmanager-992.png[]
现在,单击上面对话框中的“确定”时什么也不会发生。在下一个步骤中,我们将添加一些 JPA 代码,用于处理更改的持久性。
* 接下来,我们会添加 JPA 代码以持久保留更改。要执行此操作,请替换注释 "//Implement your save functionality here."。应使用以下代码替换该注释:
[source,java]
----
EntityManager entityManager = Persistence.createEntityManagerFactory("CustomerLibraryPU").createEntityManager();
entityManager.getTransaction().begin();
Customer c = entityManager.find(Customer.class, customer.getCustomerId());
c.setName(jTextField1.getText());
c.setCity(jTextField2.getText());
entityManager.getTransaction().commit();
----
当前未定义 ``customer.getCustomerId()()`` 中的 "customer"。请在 ``resultChanged`` 中添加下面的粗体行(在类顶部声明 ``Customer customer;`` 后),以使当前 ``Customer`` 对象设置 ``customer`` ,上面的持久性代码将使用它获取当前 ``Customer`` 对象的 ID
[source,java]
----
@Override
public void resultChanged(LookupEvent lookupEvent) {
Lookup.Result r = (Lookup.Result) lookupEvent.getSource();
Collection<Customer> c = r.allInstances();
if (!c.isEmpty()) {
for (Customer customer : c) {
*customer = cust;*
jTextField1.setText(customer.getName());
jTextField2.setText(customer.getCity());
}
} else {
jTextField1.setText("[no name]");
jTextField2.setText("[no city]");
}
}
----
* 运行应用程序并更改一些数据。目前,没有“刷新”功能(将在下一步中添加),因此,要查看更改的数据,请重新启动应用程序。例如,此处的树状分层结构显示了保留的 "Toyota Motor Co" 客户名称:
image::images/crud_68dbmanager-993.png[]
[start=9]
1. 第四,我们需要添加刷新客户查看器的功能。您可能希望添加一个定期刷新查看器的 ``Timer`` 。而在此例中,我们将向根节点添加一个“刷新”菜单项,以便用户可以手动刷新查看器。
* ``CustomerViewer`` 模块的主包中,创建一个新的 ``Node`` ,用于替换在查看器中用作子对象根的 ``AbstractNode`` 。注意,我们还会将一个“刷新”操作绑定到新的根节点。
[source,java]
----
public class CustomerRootNode extends AbstractNode {
public CustomerRootNode(Children kids) {
super(kids);
setDisplayName("Root");
}
@Override
public Action[] getActions(boolean context) {
Action[] result = new Action[]{
new RefreshAction()};
return result;
}
private final class RefreshAction extends AbstractAction {
public RefreshAction() {
putValue(Action.NAME, "Refresh");
}
public void actionPerformed(ActionEvent e) {
CustomerTopComponent.refreshNode();
}
}
}
----
* 将以下方法添加到 ``CustomerTopComponent`` ,用于刷新视图:
[source,java]
----
public static void refreshNode() {
EntityManager entityManager = Persistence.createEntityManagerFactory("CustomerLibraryPU").createEntityManager();
Query query = entityManager.createQuery("SELECT c FROM Customer c");
List<Customer> resultList = query.getResultList();
em.setRootContext(new *CustomerRootNode*(Children.create(new CustomerChildFactory(resultList), true)));
}
----
现在,用对以上方法的调用替换 ``CustomerTopComponent`` 构造函数中的以上代码。在上面的突出显示部分,我们可以看到现在使用的是 ``CustomerRootNode`` ,而不是 ``AbstractNode`` ``CustomerRootNode`` 包括“刷新”操作,此操作将调用以上代码。
* 在保存功能中,添加对上述方法的调用,以便每次保存数据时,都会自动进行刷新。可以使用不同方法为保存功能实现此扩展。例如,您可能希望创建一个包含刷新操作的新模块。然后,将在查看器模块和编辑器模块之间共享该模块,以便为两者提供相同的功能。
* 再次运行应用程序,注意,您拥有了一个新的根节点,其中带有“刷新”操作。
image::images/crud_68dbmanager-994.png[]
* 更改一些数据并保存,调用“刷新”操作,注意,将更新查看器。
现在,您已学会了如何让 NetBeans 平台处理对 ``JTextField`` 所做的更改。当文本发生更改时,即会启用或禁用 NetBeans 平台的“撤销”和“重做”按钮。此外,还会正确启用和禁用“保存”按钮,让用户将更改的数据保存到数据库。
=== 创建
在此部分,将允许用户在数据库中创建一个新的条目。
[start=1]
1. 右键单击 ``CustomerEditor`` 模块,然后选择“新建操作”。使用“新建操作”向导创建一个新的“始终启用”操作。新的操作应显示在工具栏和/或菜单栏中的任意位置。在向导的下一步中,调用操作 ``NewAction``
确保有一个 16x16 的图标,当希望从工具栏调用此操作时,向导将强制选择此图标。
[start=2]
1. 在新建操作中,使 ``TopComponent`` 处于打开状态,并使 ``JTextField`` 保留空白:
[source,java]
----
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
public final class NewAction implements ActionListener {
public void actionPerformed(ActionEvent e) {
EditorTopComponent tc = EditorTopComponent.getDefault();
tc.resetFields();
tc.open();
tc.requestActive();
}
}
----
此操作将实现 ``ActionListener`` 类,此类通过层文件中的条目绑定到应用程序,并由“新建操作”向导在此处生成。设想一下将现有的 Swing 应用程序移植到 NetBeans 平台会有多么容易,因为您将可以使用与原始应用程序中相同的 ``Action`` 类,而无需重写这些类以符合 NetBeans 平台提供的 ``Action`` 类的标准!
``EditorTopComponent`` 中,添加以下方法以重置 ``JTextField`` 并创建新的 ``Customer`` 对象:
[source,java]
----
public void resetFields() {
customer = new Customer();
jTextField1.setText("");
jTextField2.setText("");
}
----
[start=3]
1. ``SaveCookie`` 中,确保返回的 ``null`` 表示已保存新条目,而非更新了现有条目:
[source,java]
----
public void save() throws IOException {
Confirmation message = new NotifyDescriptor.Confirmation("Do you want to save \""
+ jTextField1.getText() + " (" + jTextField2.getText() + ")\"?",
NotifyDescriptor.OK_CANCEL_OPTION,
NotifyDescriptor.QUESTION_MESSAGE);
Object result = DialogDisplayer.getDefault().notify(msg);
//When user clicks "Yes", indicating they really want to save,
//we need to disable the Save button and Save menu item,
//so that it will only be usable when the next change is made
//to the text field:
if (NotifyDescriptor.YES_OPTION.equals(result)) {
fire(false);
EntityManager entityManager = Persistence.createEntityManagerFactory("CustomerLibraryPU").createEntityManager();
entityManager.getTransaction().begin();
*if (customer.getCustomerId() != null)* {
Customer c = entityManager.find(Customer.class, cude.getCustomerId());
c.setName(jTextField1.getText());
c.setCity(jTextField2.getText());
entityManager.getTransaction().commit();
} else {
*Query query = entityManager.createQuery("SELECT c FROM Customer c");
List<Customer> resultList = query.getResultList();
customer.setCustomerId(resultList.size()+1);
customer.setName(jTextField1.getText());
customer.setCity(jTextField2.getText());
//add more fields that will populate all the other columns in the table!
entityManager.persist(customer);
entityManager.getTransaction().commit();*
}
}
}
----
[start=4]
1. 再次运行应用程序,并向数据库中添加一个新客户:
=== 删除
在此部分,将使用户能够删除数据库中选定的条目。使用上面介绍的概念和代码,自己实现“删除”操作。
[start=1]
1. 创建一个新的操作 ``DeleteAction`` 。确定要将其绑定到 "Customer" 节点,还是绑定到工具栏、菜单栏、快捷键或上述内容的组合。根据要绑定到的位置,您需要在代码中使用不同的方法。再次阅读教程以获取帮助,特别参见如何创建“新建”操作的部分,并将其与根节点上的“刷新”操作进行比较。
[start=2]
1. 获取当前 ``Customer`` 对象,返回“您是否确定?”对话框,然后删除该条目。有关此阶段的相关帮助,请再次阅读教程,重点查看实现“保存”功能的部分。现在不是保存,而是从数据库中删除条目。
== 另请参见
NetBeans 平台 CRUD 教程到此结束。本文档介绍了如何针对给定数据库创建一个带有 CRUD 功能的新 NetBeans 平台应用程序。有关创建和开发应用程序的更多信息,请参见以下资源:
* link:https://netbeans.apache.org/kb/docs/platform_zh_CN.html[NetBeans 平台学习资源]
* link:http://bits.netbeans.org/dev/javadoc/[NetBeans API Javadoc]