...

понедельник, 2 сентября 2013 г.

[Из песочницы] GWT-Platform основы работы с презентерами

Всем хабражителям доброго времени суток!

Я начинающий Java-программист и так уж получилось, что свою карьеру я начинаю с разработки серьезного приложения на GWT. На хабре довольно много статей на тему GWT, однако почему-то совсем нет информации о замечательном фреймворке GWT-Platform. Подробно познакомиться с данным фреймворком можно тут, а я расскажу вкратце об основах работы на примере простого приложения.



Наше приложение будет содержать в себе навигационную панель, на которой расположены кнопки для переключения текущего вида. А также две колонки, в которые мы будем вставлять нужный контент в зависимости от ситуации. Две колонки я сделал для наглядности. В реальной жизни понадобилась бы одна конечно.


Если нажать на кнопку в навбаре, то откроется либо левая часть приложения, либо правая с бессмысленным текстом.


По умолчанию открывается левая колонка, на которой находится кнопка вызова диалогового окна для ввода имени и фамилии. После нажатия кнопки подтверждения эти данные передаются в правую колонку и отображаются.









Итак, для начала нам нужно создать GWT проект в IDE. Для работы с GWTP нам потребуется добавить в проект библиотеки: guice-2.0.jar, guice-3.0.jar, gwtp-all-1.0.jar, aopalliance.jar, guice-assistedinject-3.0.jar. Также я добавил gwt-bootstrap-2.2.2.0-SNAPSHOT.jar чтобы добавить “красоты” приложению.



Можно установить в Eclipse gwt-platform плагин. Он сильно облегчает жизнь. С его помощью можно создавать как новые проекты, так и связки презентер-вью. Качается по этой ссылке: plugin.gwt-platform.googlecode.com/hg/update


Приступим:

Нужно создать клиентский модуль и Ginjector. Если приложение создавать с помощью плагина, то они будут созданы автоматически:

В методе configure () мы будем биндить наши презентеры с интерфейсами и имплементацией их вью.



public class ClientModule extends AbstractPresenterModule {

@Override
protected void configure() {
install(new DefaultModule(ClientPlaceManager.class));

bindPresenter(MainPagePresenter.class, MainPagePresenter.MyView.class,
MainPageView.class, MainPagePresenter.MyProxy.class);

bindConstant().annotatedWith(DefaultPlace.class).to(NameTokens.main);
}
}



@GinModules({ DispatchAsyncModule.class, ClientModule.class })
public interface ClientGinjector extends Ginjector {

EventBus getEventBus();

PlaceManager getPlaceManager();

Provider<MainPagePresenter> getMainPagePresenter();
}


Далее наша точка входа: Тут мы говорим нашему placemanager перейти на текущую страницу (place). То есть если у нас в адресной строке браузера был введен какой-то токен, определяющий необходимый place, то мы попадем туда. Конечно при условии что мы имеем доступ. (За это может отвечать например GateKeeper).



public class HabraTest implements EntryPoint {

private final ClientGinjector ginjector = GWT.create(ClientGinjector.class);

@Override
public void onModuleLoad() {
DelayedBindRegistry.bind(ginjector);
ginjector.getPlaceManager().revealCurrentPlace();
}
}


Не буду заострять внимание на работе с place. На хабре уже было много замечательных статей по GWT. Например эта.


Я же покажу как можно создавать небольшие GWT приложения без использования place (точнее с одним place).


Для начала создадим главный презентер нашего приложения:



public class MainPagePresenter extends
Presenter<MainPagePresenter.MyView, MainPagePresenter.MyProxy> implements MainPageUiHandlers, FirstPageEvent.Handler{

public interface MyView extends View, HasUiHandlers<MainPageUiHandlers> {
}

// идентификаторы слотов для вставки соответствующего презентера
public final static Object SLOT_FIRST_PAGE = new Object();
public final static Object SLOT_SECOND_PAGE = new Object();

//вложенные презентеры
private FirstPagePresenter firstPagePresenter;
private SecondPagePresenter secondPagePresenter;

@ProxyStandard
@NameToken(NameTokens.main)
public interface MyProxy extends ProxyPlace<MainPagePresenter> {
}

private EventBus eventBus;
private final PlaceManager placeManager;

// инжектим вложенные презентеры
@Inject
public MainPagePresenter(final EventBus eventBus, final MyView view,
FirstPagePresenter firstPagePresenter,
SecondPagePresenter secondPagePresenter,
final MyProxy proxy, final PlaceManager placeManager) {
super(eventBus, view, proxy);
this.placeManager = placeManager;
this.firstPagePresenter = firstPagePresenter;
this.secondPagePresenter = secondPagePresenter;
this.eventBus = eventBus;
// назначаем себя обработчиком событий вью
getView().setUiHandlers(this);
eventBus.addHandler(FirstPageEvent.getType(), this);
}

// внедряем себя в главный презентер приложения
@Override
protected void revealInParent() {
RevealRootContentEvent.fire(this, this);
}

@Override
protected void onBind() {
super.onBind();
// по умолчанию будет загружена первая страница
getView().setInSlot(SLOT_FIRST_PAGE, firstPagePresenter);
}

// вызывается при нажатии левой кнопки в MainPageView
@Override
public void onRightBtnClicked() {
showRightContent();
MainPageEvent mainPageEvent = new MainPageEvent( MainPageEvent.Action.SHOW_LOREM_IPSUM);
eventBus.fireEvent(mainPageEvent);
}
// аналогично при нажатии правой
@Override
public void onLeftBtnClicked() {
showLeftContent();

}

public void showLeftContent() {
removeFromSlot(SLOT_SECOND_PAGE, secondPagePresenter);
getView().setInSlot(SLOT_FIRST_PAGE, firstPagePresenter);
}

public void showRightContent() {
removeFromSlot(SLOT_FIRST_PAGE, firstPagePresenter);
getView().setInSlot(SLOT_SECOND_PAGE, secondPagePresenter);
}

@Override
public void onFirstPageEvent(FirstPageEvent event) {
// закрываем левый контент и открываем правый, в который через эвент передаем имя и фамилию
showRightContent();
MainPageEvent mainPageEvent = new MainPageEvent( MainPageEvent.Action.SHOW_FORM_RESULT, event.getFirstName(), event.getLastName());
eventBus.fireEvent(mainPageEvent);

}
}


Обратите внимание на то что мы заинжектили в конструкторе FirstPagePresenter firstPagePresenter, SecondPagePresenter secondPagePresenter.

Это будут presenter — виджеты представляющие левую и правую часть приложения (то есть в теории отдельные страницы);


В GWTP есть три основных типа презентеров:



  • Перезентеры, которые являются еще и place

  • Перезентеры-виджеты(PresenterWidget)

  • Презентеры-виджеты, представляющие собой Popup окно


Presenter-place испульзуются для создания отдельных страниц приложения, для навигации по которым можно использовать так называемые токены, добавленые в адрессной строке браузера.

Каждый такой презентер должен содержать аннотацию, которая указывает какой токен привязан к презентеру.

В нашем случае мы используем только один такой презентер.


Для смены «страниц» мы будем использовать систему слотов и презентер-виджеты помещенные в слоты.

Презентер-виджет это презентер который не обязательно является синглтоном. Он может имет множество независимых instance.

Благодаря системе слотов мы можем бесконечно вкладывать презентеры внутри других презентеров. Чтобы поместить презентер-виджет в другой презентер, нам необходимо определить слоты в родительском презентере и переопределить метод setInSlot() во вью родительского презентера.


В классе MainPagePresenter видно что слот это просто Object:



public final static Object SLOT_FIRST_PAGE = new Object();
public final static Object SLOT_SECOND_PAGE = new Object();


В соответствующем вью определяем правила вставки презентеров в слот:



public class MainPageView extends ViewWithUiHandlers<MainPageUiHandlers> implements MainPagePresenter.MyView {

// главная панель приложения
@UiField HTMLPanel main;
// навигационная панель
@UiField ResponsiveNavbar navbar;

// кнопки навигации
@UiField Button firstPageBtn, secondPageBtn;

private static MainPageViewUiBinder uiBinder = GWT
.create(MainPageViewUiBinder.class);

interface MainPageViewUiBinder extends UiBinder<Widget, MainPageView> {
}

// колонки для вставки контента
@UiField Column leftColumn, rightColumn;

@Inject
public MainPageView() {
uiBinder.createAndBindUi(this);
navbar.setInverse(true);
//обработчики для кнопок
firstPageBtn.addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
getUiHandlers().onLeftBtnClicked();
}
});

secondPageBtn.addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
getUiHandlers().onRightBtnClicked();
}
});
}

@Override
public Widget asWidget() {
return main;
}

// переопределяем метод вставки презентеров в слот
@Override
public void setInSlot(Object slot, IsWidget content) {
if(slot == MainPagePresenter.SLOT_FIRST_PAGE ) {
leftColumn.add(content);
}
else if(slot == MainPagePresenter.SLOT_SECOND_PAGE ){
rightColumn.add(content);
}
else {
super.setInSlot(slot, content);
}
}

// аналогично переопределяем метод удаления из слота
@Override
public void removeFromSlot(Object slot, IsWidget content) {
if(slot == MainPagePresenter.SLOT_FIRST_PAGE ) {
leftColumn.remove(content);
}
else if(slot == MainPagePresenter.SLOT_SECOND_PAGE ){
rightColumn.remove(content);
}
else {
super.removeFromSlot(slot, content);
}
}
}


Все довольно просто: setInSlot() принимает в себя презентер и соответствующий ему слот.

Мы просто указываем в какой виджет поместить этот презентер. В данном случае это две бутстраповские колонки leftColumn и rightColumn.

Хотя я повторюсь в данном случае уместней было бы помещать все в одну колонку, чтобы имитировать переход по страницам.


Далее наши презентер-виджеты и их вью:



public class FirstPagePresenter extends
PresenterWidget<FirstPagePresenter.MyView> implements FirstPageUiHandlers, PopupEvent.Handler{

public interface MyView extends View, HasUiHandlers<FirstPageUiHandlers> {
}

// попап с формой
FirstPagePopupPresenter firstPagePopupPresenter;
EventBus eventBus;

@Inject
public FirstPagePresenter(final EventBus eventBus, final MyView view,
FirstPagePopupPresenter firstPagePopupPresenter ) {
super(eventBus, view);
this.firstPagePopupPresenter = firstPagePopupPresenter;
this.eventBus = eventBus;
getView().setUiHandlers(this);
// назначаем себя хендлером PopupEvent
eventBus.addHandler(PopupEvent.getType(), this);
}

@Override
public void onShowFormBtnClicked() {
// показываем всплывающее окно с формой
showForm(true);
}

private void showForm(boolean show) {
if(show){
addToPopupSlot(firstPagePopupPresenter, true);
firstPagePopupPresenter.getView().show();
}
else {
removeFromPopupSlot(firstPagePopupPresenter);
}
}

@Override
public void onPopupEvent(PopupEvent event) {
showForm(false);
eventBus.fireEvent(new FirstPageEvent(event.getFirstName(), event.getLastName()));
}
}


Обратите внимание что я заинжектил некий FirstPagePopupPresenter firstPagePopupPresenter.(код будет ниже). Это наше всплывающее окно с формой. Аналогично можно инжектить любые презентер-виджеты в любом количестве и с любой вложенностью. Главное не нарушать иерархию.



public class FirstPageView extends ViewWithUiHandlers<FirstPageUiHandlers> implements
FirstPagePresenter.MyView {

private final Widget widget;
@UiField Button showFormBtn;

public interface Binder extends UiBinder<Widget, FirstPageView> {
}

@Inject
public FirstPageView(final Binder binder) {
widget = binder.createAndBindUi(this);

showFormBtn.addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
getUiHandlers().onShowFormBtnClicked();
}
});
}

@Override
public Widget asWidget() {
return widget;
}
}


Во вью особо ничего интересного, кроме того, что оно наследует типизированный класс ViewWithUiHandlers.

Так как мы не хотим нарушать принципов MVP, то и не можем обращаться к презентеру напрямую из вью( наоборот можем). Для этого мы используем интерфейсы. О нажатии кнопки мы сообщаем с помощью getUiHandlers().onShowFormBtnClicked();



public interface FirstPageUiHandlers extends UiHandlers{

void onShowFormBtnClicked();
}


getUiHandlers() возвращает нам интерфейс FirstPageUiHandlers, в котором мы указываем методы, которые должны быть реализованы в соответствующем презентере. Естественно что презентер должен имплементировать данный интерфейс, а вложенный в него интерфейс MyView должен наследовать типизированный интерфейс HasUiHandlers. И главное не забыть в презентере назначить себя обработчиком для событий вью — getView().setUiHandlers(this);


Далее презентер и соответствующий вью второй страницы:



public class SecondPagePresenter extends
PresenterWidget<SecondPagePresenter.MyView> implements MainPageEvent.Handler {

public interface MyView extends View {
void showLoremIpsum();
void showFormInfo(String firstName, String lastName);
}

EventBus eventBus;

@Inject
public SecondPagePresenter(final EventBus eventBus, final MyView view) {
super(eventBus, view);
this.eventBus = eventBus;
eventBus.addHandler(MainPageEvent.getType(), this);
}

@Override
public void onMainPageEvent(MainPageEvent event) {
switch(event.getAction()) {
case SHOW_FORM_RESULT:
showFormInfoWidget(event.getFirstName(), event.getLastName());
break;
case SHOW_LOREM_IPSUM:
showLoremIpsumWidget();
break;
default:
break;
}
}

private void showLoremIpsumWidget() {
getView().showLoremIpsum();
}

private void showFormInfoWidget(String firstName, String lastName) {
getView().showFormInfo( firstName, lastName);
}



public class SecondPageView extends ViewImpl implements
SecondPagePresenter.MyView {

private final Widget widget;

@UiField FlowPanel contentPanel;

public interface Binder extends UiBinder<Widget, SecondPageView> {
}

@Inject
public SecondPageView(final Binder binder) {
widget = binder.createAndBindUi(this);
}

@Override
public Widget asWidget() {
return widget;
}

@Override
public void showLoremIpsum() {
contentPanel.clear();
contentPanel.add(new LoremIpsumWidget());
}

@Override
public void showFormInfo(String firstName, String lastName) {
contentPanel.clear();
contentPanel.add(new FormInfoWidget(firstName, lastName));
}
}


Тут особо ничего интересного и нового для разработчика на GWT. Общение между презентерами происходит посредством стандартных эвентов ( GwtEvent ).


И наконец попап с формой:



public class FirstPagePopupPresenter extends
PresenterWidget<FirstPagePopupPresenter.MyView> implements PopupUiHandlers {

public interface MyView extends PopupView , HasUiHandlers<PopupUiHandlers>{
}

EventBus eventBus;

@Inject
public FirstPagePopupPresenter(final EventBus eventBus, final MyView view) {
super(eventBus, view);
this.eventBus = eventBus;
getView().setUiHandlers(this);
}

@Override
public void onSubmitBtnClicked(String firstName, String lastName) {
eventBus.fireEvent(new PopupEvent(firstName, lastName));
}
}



public class FirstPagePopupView extends PopupViewWithUiHandlers<PopupUiHandlers> implements
FirstPagePopupPresenter.MyView {

@UiField PopupPanel main;
@UiField Button submitBtn;
@UiField TextBox firstName, lastName;

public interface Binder extends UiBinder<Widget, FirstPagePopupView> {
}

@Inject
public FirstPagePopupView(final EventBus eventBus, final Binder binder) {
super(eventBus);
binder.createAndBindUi(this);
main.setAnimationEnabled(true);
main.setModal(true);
main.setGlassEnabled(true);
submitBtn.addClickHandler(new ClickHandler() {

@Override
public void onClick(ClickEvent event) {
getUiHandlers().onSubmitBtnClicked(firstName.getValue(), lastName.getValue());
}
});
}

@Override
public Widget asWidget() {
return main;
}
}


Как видно попап тоже является презентер-виджетом, но интерфейс его вью должен наследовать PopupView. И еще главная панель вью должна быть обязательно PopupPanel, ну или наследником данного класса. Еще одно отличие от обычных презентер-виджетов — чтобы показать попап на странице не нужен слот и панель для вставки. Достаточно использовать метод addToPopupSlot();


Также во всех связках презентер-вью использован uibinder. Соответствующие *ui.xml файлы я не выкладываю. Там в принципе ничего для GWT-разработчиков интересного нет.


Сам проект будет доступен некоторое время по этому адресу


И так а сейчас пробежимся по проекту что бы описать что происходит и как связаны презентеры между собой:


При загрузке MainPagePresenter в переопределенном методе onBind() мы сразу же ставим в слот презентер первой страницы:



@Override
protected void onBind() {
super.onBind();
getView().setInSlot(SLOT_FIRST_PAGE, firstPagePresenter);
}


(Про жизненный цикл презентеров и методы onBind(), onUnbind, onReveal(), onReset(), onHide() я бы хотел рассказать в следующей статье.)


Соответственно в левой части экрана появляется вью FirstPagePresenter'a. При клике на кнопку мы вызываем имплементацию метода onShowFormBtnClicked() в FirstPagePresenter описанного в интерфейсе FirstPageUiHandlers


Вызов:



showFormBtn.addClickHandler(new ClickHandler() {
@Override
public void onClick(ClickEvent event) {
getUiHandlers().onShowFormBtnClicked();
}
});


в FirstPagePresenter' e происходит следующее:



addToPopupSlot(firstPagePopupPresenter, true);


Мы сетим презентер попапа в слот. Как я уже упоминал, для попапов слот не нужно определять. Единственное условие, что презентер из которого вызывается попап должен сам находится в слоте родителя и так далее по цепочке. Второй параметр в методе addToPopupSlot() указывает центровать ли попап в окне приложения(метод имеет несколько перегрузок и данный параметр в общем-то необязателен).


После того как попап появляется в окне, мы можем ввести туда какие-то данные и нажать кнопку подтверждения. Далее по аналогичной схеме вью попапа через getUiHandlers() вызывает обработчик в своем презентере, а тот в свою очередь кидает через EventBus эвент, на который подписан FirstPagePresenter (если кому-то интересно, то про эвенты в GWT я бы мог рассказать в следующей заметке):



@Override
public void onPopupEvent(PopupEvent event) {
showForm(false);
eventBus.fireEvent(new FirstPageEvent(event.getFirstName(), event.getLastName()));
}


Сначала в методе showForm() мы удаляем попап из слота:



removeFromPopupSlot(firstPagePopupPresenter);


Затем кидаем новый эвент ( теперь это FirstPageEvent) дальше. На него подписан наш MainPagePresenter:



@Override
public void onFirstPageEvent(FirstPageEvent event) {
// закрываем левый контент и открываем правый , в который через эвент передаем имя и фамилию
showRightContent();
MainPageEvent mainPageEvent = new MainPageEvent( MainPageEvent.Action.SHOW_FORM_RESULT, event.getFirstName(), event.getLastName());
eventBus.fireEvent(mainPageEvent);
}


Получив его MainPagePresenter удаляет из слота первую страницу и вставляет вторую:



public void showRightContent() {
removeFromSlot(SLOT_FIRST_PAGE, firstPagePresenter);
getView().setInSlot(SLOT_SECOND_PAGE, secondPagePresenter);
}


Далее он полылает уже MainPageEvent дальше. В него сетит не только имя и фамилию, но и Action.


Наш SecondPagePresenter получив эвент в методе onMainPageEvent() решает что показать на странице. В данном случае это обычные виджеты без презентеров.



@Override
public void onMainPageEvent(MainPageEvent event) {
switch(event.getAction()) {
case SHOW_FORM_RESULT:
showFormInfoWidget(event.getFirstName(), event.getLastName());
break;
case SHOW_LOREM_IPSUM:
showLoremIpsumWidget();
break;
default:
break;
}
}


Вот собственно и все. Наверное, некоторым может показаться что для таких простых действий слишком много кода, но:



  • Мы не нарушаем принципов MVP — вью ничего не должно знать о своем презентере

  • Разделив приложение на модули код становится reusable и более гибким


Также наверняка некоторые возмутятся — зачем такие длинные цепочки передачи эвентов? Однако можно заметить, что получив эвент, презентер, перед отправкой следующего, делает какие-либо операции. Например, удаляет ненужные более презентеры или обрабатывает как-то полученные данные.


В общем надеюсь данная заметка окажется кому-либо полезной и он обратит свой взор в сторону GWT-Platform.


PS: Прошу прощения за некую сумбурность повествования и возможные ошибки. Это мой первый пост на IT-тематику. Обоснованная критика и советы очень приветствуются.


This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at fivefilters.org/content-only/faq.php#publishers. Five Filters recommends:



Комментариев нет:

Отправить комментарий