GWT MVP
建立任何大规模的应用都有其障碍,GWT的应用程序也不例外。多个开发者基于同样的代码并行开发,往往会让代码变得混乱,难于维护。为了帮助理清头绪,我们介绍设计模式到我们的项目中。
有各种各样的设计模式供我们选择:Presentation-abstraction-control, Model-view-controller, Model-view-presenter等等。每种设计模式都有自己的优点,但是我们发现Model-view-presenter (MVP)这种模式最适合GWT。原因有二:
- MVP模式允许多个开发者共同开发,互不影响
- MVP模式使我们能够减少使用GWTTestCase,这依赖于浏览器的存在;而且,对于大部分的代码,只需要写的轻量级(快速)的JRE测试(不需要浏览器支持)
简单的例子
一旦理解了MVP的基本原理,创建一个基于MVP的应用将会变得非常容易。为了帮助理解,这里提供了一个简单的应用Contacts作为例子。
在开始之前,要先介绍一下基本组件:
- Model
- View
- Presenter
Model
模型包含了业务对象,在本例中,有
- Contact:联系人,包含了姓名、email等信息
- ContactDetails:Contact的轻量版本,仅仅包含了唯一标识和显示名。更少的序列化和传输会让联系人列表检索效率更高
View
视图包含了所有的UI组件,负责UI组件的布局。它没有Model的概念,就是说它并不知道它将要展示的是Contact联系人列表。 本例中,视图有
- ContactsView:显示联系人列表
- EditContactView:用来添加或者编辑联系人
Presenter
Presenter包含了本例中所有的逻辑,如历史管理、视图转换、数据同步等。一个普遍的规则是,对每一个视图,都需要一个Presenter来控制视图和处理事件Event。 本例中,包含
- ContactsPresenter
- EditContactPresenter
可以看到Presenter与View是一一对应的。
整个示例工程的结构如下:
接下来,我们来看看这些组件是如何进行交互的。
绑定Presenter和View
先看看示例中ContactView的界面
该视图有三个组件,两个按钮和一个表格。对应的Presenter需要做
- 响应按钮点击事件
- 用数据填充列表
- 响应列表点击事件
- 查询被选中的联系人信息
在ContactsPresenter中,定义一个Display的接口
public class ContactsPresenter implements Presenter {
//...
public interface Display extends HasValue<List<String>> {
HasClickHandlers getAddButton();
HasClickHandlers getDeleteButton();
HasClickHandlers getList();
void setData(List<String> data);
int getClickedRow(ClickEvent event);
List<Integer> getSelectedRows();
Widget asWidget();
}
}
方法setData()用来获取Model数据到View,而无需关心Model的内部结构。这样的优点在于当Model发生变化的时候,不用修改View的代码。我们来看看它是如何获取服务端的数据:
public class ContactsPresenter implements Presenter {
//...
private void fetchContactDetails() {
rpcService.getContactDetails(new AsyncCallback<ArrayList<ContactDetails>>() {
public void onSuccess(ArrayList<ContactDetails> result) {
contacts = result;
List<String> data = new ArrayList<String>();
for (int i = 0; i < result.size(); ++i) {
data.add(contacts.get(i).getDisplayName());
}
display.setData(data);
}
public void onFailure(Throwable caught) {
//...
}
});
}
}
监听UI的事件:
public class ContactsPresenter implements Presenter {
//...
public void bind() {
display.getAddButton().addClickHandler(new ClickHandler() {
public void onClick(ClickEvent event) {
eventBus.fireEvent(new AddContactEvent());
}
});
display.getDeleteButton().addClickHandler(new ClickHandler() {
public void onClick(ClickEvent event) {
deleteSelectedContacts();
}
});
display.getList().addClickHandler(new ClickHandler() {
public void onClick(ClickEvent event) {
int selectedRow = display.getClickedRow(event);
if (selectedRow >= 0) {
String id = contacts.get(selectedRow).getId();
eventBus.fireEvent(new EditContactEvent(id));
}
}
});
}
}
响应UI上面的操作,例如删除选中的记录:
public class ContactsPresenter implements Presenter {
//...
private void deleteSelectedContacts() {
List<Integer> selectedRows = display.getSelectedRows();
ArrayList<String> ids = new ArrayList<String>();
for (int i = 0; i < selectedRows.size(); ++i) {
ids.add(contactDetails.get(selectedRows.get(i)).getId());
}
rpcService.deleteContacts(ids, new AsyncCallback<ArrayList<ContactDetails>>() {
public void onSuccess(ArrayList<ContactDetails> result) {
contactDetails = result;
List<String> data = new ArrayList<String>();
for (int i = 0; i < result.size(); ++i) {
data.add(contactDetails.get(i).getDisplayName());
}
display.setData(data);
}
public void onFailure(Throwable caught) {
//...
}
});
}
}
再次申明,为了充分了解MVP的优点,Presenter应该不认识任何基础组件代码。只要把View封装在Display的接口中,这样鱼和熊掌都可兼得,一方面减少了GWT各组件之间的联系,另一方面依然可以实例化一个Display对象到面板上。