MVP 活动和位置

GWT 2.1 引入了用于浏览器历史管理的内置框架。活动和位置框架允许您在应用程序中创建可书签的 URL,从而允许浏览器的后退按钮和书签按预期工作。它基于 GWT 的 历史机制,可以与 MVP 开发结合使用,但不是必需的。

严格来说,MVP 架构不涉及浏览器历史管理,但活动和位置可以与 MVP 开发结合使用,如本文所示。如果您不熟悉 MVP,您可能需要先阅读以下文章

定义

活动只是表示用户正在执行的操作。活动不包含任何 Widgets 或 UI 代码。活动通常会恢复状态(“唤醒”)、执行初始化(“设置”)以及加载相应的 UI(“显示”)。活动由与容器 Widget 关联的 ActivityManager 启动和停止。活动可以在活动即将停止时自动显示警告确认(例如,当用户导航到另一个位置时)。此外,ActivityManager 会在窗口即将关闭之前警告用户。

位置是一个 Java 对象,表示 UI 的特定状态。位置可以通过定义每个位置的 PlaceTokenizer 转换为 URL 历史标记(请参阅 GWT 的 历史机制),反之亦然,GWT 的 PlaceHistoryHandler 会自动更新与应用程序中每个位置相对应的浏览器 URL。

您可以在 此示例应用 中下载此处引用的所有代码。示例应用是一个简单的“Hello, World!”示例,演示了如何在 MVP 中使用活动和位置。

让我们看看使用位置和活动的 GWT 2.1 应用中的每个活动部件。

  1. 视图
  2. ClientFactory
  3. 活动
  4. 位置
  5. PlaceHistoryMapper
  6. ActivityMapper

然后,我们将看看如何将所有这些连接在一起以及它是如何工作的。

  1. 将所有内容整合在一起
  2. 工作原理
  3. 如何导航
  4. 相关资源

视图

视图只是与活动关联的 UI 部分。在 MVP 开发中,视图由一个接口定义,该接口允许基于客户端特征(例如移动设备与桌面设备)的多个视图实现,并通过避免耗时的 GWTTestCase 促进轻量级单元测试。GWT 中没有视图接口或类,视图必须实现或扩展这些接口或类;但是,GWT 2.1 引入了 IsWidget 接口,该接口由大多数 Widgets 以及 Composite 实现。如果视图确实提供了一个 Widget,则它们使用 IsWidget 扩展起来会很有用。以下来自示例应用的简单视图。

public interface GoodbyeView extends IsWidget {
    void setName(String goodbyeName);
}

相应的视图实现扩展了 Composite,这使得对特定 Widget 的依赖关系不会泄漏出去。

public class GoodbyeViewImpl extends Composite implements GoodbyeView {
    SimplePanel viewPanel = new SimplePanel();
    Element nameSpan = DOM.createSpan();

    public GoodbyeViewImpl() {
        viewPanel.getElement().appendChild(nameSpan);
        initWidget(viewPanel);
    }

    @Override
    public void setName(String name) {
        nameSpan.setInnerText("Good-bye, " + name);
    }
}

以下是一个更复杂的视图,它还定义了其对应演示者(活动)的接口。

public interface HelloView extends IsWidget {
    void setName(String helloName);
    void setPresenter(Presenter presenter);

    public interface Presenter {
        void goTo(Place place);
    }
}

Presenter 接口和 setPresenter 方法允许在视图和演示者之间进行双向通信,这简化了涉及重复 Widgets 的交互,并允许视图实现使用 UiBinder 和 @UiHandler 方法,这些方法将委托给 Presenter 接口。

HelloView 实现使用 UiBinder 和模板。

public class HelloViewImpl extends Composite implements HelloView {
    private static HelloViewImplUiBinder uiBinder = GWT
            .create(HelloViewImplUiBinder.class);

    interface HelloViewImplUiBinder extends UiBinder<Widget, HelloViewImpl> {
    }

    @UiField
    SpanElement nameSpan;
    @UiField
    Anchor goodbyeLink;
    private Presenter presenter;
    private String name;

    public HelloViewImpl() {
        initWidget(uiBinder.createAndBindUi(this));
    }

    @Override
    public void setName(String name) {
        this.name = name;
        nameSpan.setInnerText(name);
    }

    @UiHandler("goodbyeLink")
    void onClickGoodbye(ClickEvent e) {
        presenter.goTo(new GoodbyePlace(name));
    }

    @Override
    public void setPresenter(Presenter presenter) {
        this.presenter = presenter;
    }
}

注意 @UiHandler 的使用,它将委托给演示者。以下是相应的模板

<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
<ui:UiBinder xmlns:ui="urn:ui:com.google.gwt.uibinder"
             xmlns:g="urn:import:com.google.gwt.user.client.ui">
    <ui:style>
        .important {
            font-weight: bold;
        }
    </ui:style>
    <g:HTMLPanel>
        Hello,
        <span class="{style.important}" ui:field="nameSpan" />
        <g:Anchor ui:field="goodbyeLink" text="Say good-bye"></g:Anchor>
    </g:HTMLPanel>
</ui:UiBinder>

由于 Widget 创建涉及 DOM 操作,因此创建视图的成本相对较高。因此,最好使它们可重用,而一个相对简单的做法是通过视图工厂,该工厂可能是较大 ClientFactory 的一部分。

ClientFactory

使用活动和位置不需要 ClientFactory;但是,使用工厂或依赖项注入框架(如 GIN)来获取对应用程序中需要使用的对象的引用(如事件总线)会很有帮助。我们的示例使用 ClientFactory 来提供 EventBus、GWT PlaceController 和视图实现。

public interface ClientFactory {
    EventBus getEventBus();
    PlaceController getPlaceController();
    HelloView getHelloView();
    GoodbyeView getGoodbyeView();
}

使用 ClientFactory 的另一个优点是,您可以将其与 GWT 延迟绑定一起使用,以根据 user.agent 或其他属性使用不同的实现类。例如,您可能使用 MobileClientFactory 来提供与默认 DesktopClientFactory 不同的视图实现。要做到这一点,请在 onModuleLoad() 中使用 GWT.create 实例化您的 ClientFactory,如下所示

ClientFactory clientFactory = GWT.create(ClientFactory.class);

在 .gwt.xml 中指定实现类

<!-- Use ClientFactoryImpl by default -->
    <replace-with class="com.hellomvp.client.ClientFactoryImpl">
    <when-type-is class="com.hellomvp.client.ClientFactory"/>
    </replace-with>

您可以使用 <when-property-is> 根据 user.agent、区域设置或您定义的其他属性指定不同的实现。 mobilewebapp 示例应用程序定义了一个“formfactor”属性,用于为移动设备、平板电脑和桌面设备选择不同的视图实现。

以下是示例应用的 ClientFactory 的默认实现

public class ClientFactoryImpl implements ClientFactory {
    private final EventBus eventBus = new SimpleEventBus();
    private final PlaceController placeController = new PlaceController(eventBus);
    private final HelloView helloView = new HelloViewImpl();
    private final GoodbyeView goodbyeView = new GoodbyeViewImpl();

    @Override
    public EventBus getEventBus() {
        return eventBus;
    }
    ...
}

活动

活动类实现 com.google.gwt.activity.shared.Activity。为了方便起见,您可以扩展 AbstractActivity,它提供所有必需方法的默认(null)实现。以下是一个 HelloActivity,它只是向命名用户问好

public class HelloActivity extends AbstractActivity implements HelloView.Presenter {
    // Used to obtain views, eventBus, placeController
    // Alternatively, could be injected via GIN
    private ClientFactory clientFactory;
    // Name that will be appended to "Hello,"
    private String name;

    public HelloActivity(HelloPlace place, ClientFactory clientFactory) {
        this.name = place.getHelloName();
        this.clientFactory = clientFactory;
    }

    /**
     * Invoked by the ActivityManager to start a new Activity
     */
    @Override
    public void start(AcceptsOneWidget containerWidget, EventBus eventBus) {
        HelloView helloView = clientFactory.getHelloView();
        helloView.setName(name);
        helloView.setPresenter(this);
        containerWidget.setWidget(helloView.asWidget());
    }

    /**
     * Ask user before stopping this activity
     */
    @Override
    public String mayStop() {
        return "Please hold on. This activity is stopping.";
    }

    /**
     * Navigate to a new Place in the browser
     */
    public void goTo(Place place) {
        clientFactory.getPlaceController().goTo(place);
    }
}

首先要注意的是,HelloActivity 引用了 HelloView,HelloView 是一个视图接口,而不是一个实现。一种 MVP 编码风格在演示者中定义了视图接口。这是完全合法的;但是,没有根本原因要求活动与其对应的视图接口紧密绑定在一起。请注意,HelloActivity 还实现了视图的 Presenter 接口。这用于允许视图调用活动上的方法,这简化了涉及重复 Widgets 的使用,并允许视图实现使用 UiBinder 和 @UiHandler 方法,这些方法将委托给 Presenter 接口。

HelloActivity 构造函数接受两个参数:一个 HelloPlace 和 ClientFactory。两者对于活动来说都不是严格要求的。HelloPlace 只是让 HelloActivity 很容易地获取由 HelloPlace 表示的状态的属性(在本例中,我们要问好的用户的名称)。在构造函数中接受 HelloPlace 的实例意味着将为每个 HelloPlace 创建一个新的 HelloActivity。您也可以从工厂获取活动,但这通常更干净,因为您不必清理任何先前的状态。活动被设计为可处置的,而视图(由于需要 DOM 调用,创建成本更高)应该是可重用的。为了保持这种理念,HelloActivity 使用 ClientFactory 获取对 HelloView 以及 EventBus 和 PlaceController 的引用。

start 方法由 ActivityManager 调用,并启动操作。它更新视图,然后通过调用 setWidget 将视图交换回活动的容器 Widget。

非空 mayStop() 方法提供一个警告,该警告将在活动因窗口关闭或导航到另一个位置而即将停止时显示给用户。如果它返回 null,则不会显示此警告。

最后,goTo() 方法调用 PlaceController 导航到新的位置。PlaceController 反过来会通知 ActivityManager 停止当前活动,找到并启动与新位置关联的活动,并在 PlaceHistoryHandler 中更新 URL。

位置

为了可以通过 URL 访问,活动需要一个对应的位置。位置扩展 com.google.gwt.place.shared.Place,并且必须具有一个关联的 PlaceTokenizer,该 PlaceTokenizer 知道如何将位置的状态序列化为 URL 标记。默认情况下,URL 由位置的简单类名(如“HelloPlace”)后跟冒号(:)和 PlaceTokenizer 返回的标记组成。

public class HelloPlace extends Place {
    private String helloName;

    public HelloPlace(String token) {
        this.helloName = token;
    }

    public String getHelloName() {
        return helloName;
    }

    public static class Tokenizer implements PlaceTokenizer<HelloPlace> {
        @Override
        public String getToken(HelloPlace place) {
            return place.getHelloName();
        }

        @Override
        public HelloPlace getPlace(String token) {
            return new HelloPlace(token);
        }
    }
}

在相应的位置内声明 PlaceTokenizer 作为静态类很方便(但不是必需的)。但是,您不需要为每个位置都提供 PlaceTokenizer。应用程序中的许多位置可能不会将任何状态保存到 URL,因此它们可以扩展 BasicPlace,BasicPlace 声明一个 PlaceTokenizer,该 PlaceTokenizer 返回一个 null 标记。

PlaceHistoryMapper

PlaceHistoryMapper 声明了应用程序中可用的所有位置。您创建一个扩展 PlaceHistoryMapper 的接口,并使用 @WithTokenizers 注释列出每个标记器类。以下是我们示例中的 PlaceHistoryMapper

@WithTokenizers({HelloPlace.Tokenizer.class, GoodbyePlace.Tokenizer.class})
public interface AppPlaceHistoryMapper extends PlaceHistoryMapper
{
}

在 GWT 编译时,GWT 会根据您的接口生成(请参阅 PlaceHistoryMapperGenerator)一个扩展 AbstractPlaceHistoryMapper 的类。PlaceHistoryMapper 是您的 PlaceTokenizer 和 GWT 的 PlaceHistoryHandler 之间的链接,PlaceHistoryHandler 会同步浏览器 URL 与每个位置。

要获得对 PlaceHistoryMapper 的更多控制,您可以在 PlaceTokenizer 上使用 @Prefix 注释来更改与位置关联的 URL 的第一部分。为了获得更多控制,您可以改为实现 PlaceHistoryMapperWithFactory,并提供一个 TokenizerFactory,该工厂反过来会提供单个 PlaceTokenizer。

ActivityMapper

最后,您的应用的 ActivityMapper 将每个 Place 映射到其对应的 Activity。它必须实现 ActivityMapper,并且可能包含大量类似“if (place instanceof SomePlace) return new SomeActivity(place)”的代码。以下是我们示例应用的 ActivityMapper

public class AppActivityMapper implements ActivityMapper {
    private ClientFactory clientFactory;

    public AppActivityMapper(ClientFactory clientFactory) {
        super();
        this.clientFactory = clientFactory;
    }

    @Override
    public Activity getActivity(Place place) {
        if (place instanceof HelloPlace)
            return new HelloActivity((HelloPlace) place, clientFactory);
        else if (place instanceof GoodbyePlace)
            return new GoodbyeActivity((GoodbyePlace) place, clientFactory);
        return null;
    }
}

请注意,我们的 ActivityMapper 必须了解 ClientFactory,以便在需要时将其提供给活动。

将所有内容整合在一起

以下是所有部分在 onModuleLoad() 中如何结合在一起的

public class HelloMVP implements EntryPoint {
    private Place defaultPlace = new HelloPlace("World!");
    private SimplePanel appWidget = new SimplePanel();

    public void onModuleLoad() {
        ClientFactory clientFactory = GWT.create(ClientFactory.class);
        EventBus eventBus = clientFactory.getEventBus();
        PlaceController placeController = clientFactory.getPlaceController();

        // Start ActivityManager for the main widget with our ActivityMapper
        ActivityMapper activityMapper = new AppActivityMapper(clientFactory);
        ActivityManager activityManager = new ActivityManager(activityMapper, eventBus);
        activityManager.setDisplay(appWidget);

        // Start PlaceHistoryHandler with our PlaceHistoryMapper
        AppPlaceHistoryMapper historyMapper= GWT.create(AppPlaceHistoryMapper.class);
        PlaceHistoryHandler historyHandler = new PlaceHistoryHandler(historyMapper);
        historyHandler.register(placeController, eventBus, defaultPlace);

        RootPanel.get().add(appWidget);
        // Goes to the place represented on URL else default place
        historyHandler.handleCurrentHistory();
    }
}

工作原理

ActivityManager 跟踪在一个容器小部件的上下文中运行的所有活动。它监听 PlaceChangeRequestEvents 并通知当前活动何时请求新的 Place。如果当前活动允许更改 Place(Activity.onMayStop() 返回 null)或用户允许(通过在确认对话框中点击“确定”),ActivityManager 将丢弃当前活动并启动新的活动。为了找到新的活动,它使用您的应用程序的 ActivityMapper 来获取与请求的 Place 关联的活动。

除了 ActivityManager 之外,还有另外两个 GWT 类用于跟踪应用程序中的 Place。PlaceController 启动导航到新的 Place,并负责在执行此操作之前警告用户。PlaceHistoryHandler 在 Place 和 URL 之间提供双向映射。每当您的应用程序导航到新的 Place 时,URL 将使用表示 Place 的新令牌进行更新,以便可以将其添加到书签并保存在浏览器历史记录中。同样,当用户点击“后退”按钮或调出书签时,PlaceHistoryHandler 会确保您的应用程序加载相应的 Place。

如何导航

要导航到应用程序中的新的 Place,请在 PlaceController 上调用 goTo() 方法。这在上面的 HelloActivity 的 goTo() 方法中进行了说明。PlaceController 会警告当前活动它可能会停止(通过 PlaceChangeRequest 事件),一旦允许,它会使用新的 Place 触发 PlaceChangeEvent。PlaceHistoryHandler 监听 PlaceChangeEvents 并相应地更新 URL 历史记录令牌。ActivityManager 也监听 PlaceChangeEvents,并使用您的应用程序的 ActivityMapper 启动与新的 Place 关联的 Activity。

除了使用 PlaceController.goTo() 之外,您还可以创建一个包含新 Place 的历史记录令牌的超链接,该令牌可以通过调用 PlaceHistoryMapper.getToken() 获取。当用户导航到新的 URL(通过超链接、“后退”按钮或书签)时,PlaceHistoryHandler 会捕获来自 History 对象的 ValueChangeEvent,并调用您的应用程序的 PlaceHistoryMapper 将历史记录令牌转换为其对应的 Place。然后,它使用新的 Place 调用 PlaceController.goTo()。

对于具有多个面板(这些面板的状态应一起保存在单个 URL 中)的应用程序,您如何处理呢?您可以为每个面板使用 ActivityManager 和 ActivityMapper 来实现这一点。使用这种技术,一个 Place 可以映射到多个活动。有关更多示例,请参阅以下资源。

相关资源