pt>

środa, 1 lutego 2012

GWT + MVP4G - wzorzec w praktyce

    Dzisiejszy wpis, mój pierwszy na blogu, nie będzie dotyczył niczego dziwnego. Będzie niejako przygotowaniem dla kolejnego wpisu. Zaprezentuję połączenie GWT oraz frameworku MVP4G co samo w sobie nie jest niczym niezwykłym, aczkolwiek jest to konfiguracja bardzo owocna. 
Dlaczego warto łączyć GWT z innymi frameworkami? Otóż surowe GWT nie ułatwia szybkiego tworzenia większych aplikacji. Sami twórcy GWT polecają stosowanie wzorca MVP (Model View Presenter), jednak nie dostarczają implementacji, gdyż to co jest proponowane w oficjalnym tutorialu dla GWT woła o pomstę do Nieba! Wystarczy spojrzeć na hierarchię klas :



    Dla każdego zdarzenia konieczne jest stworzenie dwóch klas. Nie trudno się domyślić że pisanie aplikacji bardziej skomplikowanej witanie użytkownika po naciśnięciu przycisku spowoduje wykładniczy przyrost klas. Taki sposób pisania aplikacji nie przypadł mi do gustu, zapytałem jugowiczów jak ugryźć temat. Tak się dowiedziałem o MVP4G - implementacji owego wzorca. Nie będę tutaj się rozpisywał na temat zalet i wad - po prostu pokażę jak stworzyć projekt. 

Potrzebne biblioteki/narzedzia :
Na początek tworzymy nowy projekt w Eclipse - aplikacja GWT wraz z sample code :


  W następnym kroku odznaczamy "Use Google App Engine" bo nie będzie nam potrzebny ale za to zaznaczamy opcję "Generate Project Sample Code". Eclipse wygeneruje nam całą infrastrukturę projektu, która się przyda - przynajmniej w większości. Ja swój projekt nazwałem tutek_MVP4G. Zaczynamy od usunięcia niepotrzebnych plików i odwołań do nich w kodzie :
  • FieldVerifier
  • Tutek_MVP4G (wygenerowana klasa domyślnego entryPoint'a)
    Wyżej wymienione rzeczy są zbędne - usuwamy je. Jednak skoro usunęliśmy nasz entry point to jak rozpocznie się aplikacja? następny krok rozwiązuje ten problem - edycja  pliku konfiguracyjnego xml - tutek_MVP4G.gwt.xml :

<?xml version="1.0" encoding="UTF-8"?>
<module rename-to='tutek_mvp4g'>
  <!-- Inherit the core Web Toolkit stuff.                        -->
  <inherits name='com.google.gwt.user.User'/>

  <!-- Inherit the default GWT style sheet.  You can change       -->
  <!-- the theme of your GWT application by uncommenting          -->
  <!-- any one of the following lines.                            -->
  <inherits name='com.google.gwt.user.theme.clean.Clean'/>
  <!-- <inherits name='com.google.gwt.user.theme.standard.Standard'/> -->
  <!-- <inherits name='com.google.gwt.user.theme.chrome.Chrome'/> -->
  <!-- <inherits name='com.google.gwt.user.theme.dark.Dark'/>     -->
  
  <inherits name='com.mvp4g.Mvp4gModule' />

  <!-- Other module inherits                                      -->

  <!-- Specify the app entry point class.                         -->

  <!-- Specify the paths for translatable code                    -->
  <source path='client'/>
  <source path='shared'/>

</module>

    Następnie przystępujemy do tworzenia struktury pakietów projektu. W pakiecie "tutek.client" tworzymy kolejno pakiety "view", "presenter" oraz "bean".


    Pora utworzyć pierwszy widok (kolejność nie jest wymuszona) - w pakiecie "tutek.client.view" tworzymy widok - do tego celu wykorzystamy UIBinder. Oczywiście widokiem może być dowolna klasa rozszerzająca Composite, jednak użycie UIBindera nieco więcej nas nauczy na temat mechanizmów MVP4G i tworzenia przez framework widoków oraz wiązania ich z prezenterami. 


    Po utworzeniu widoku otwieramy "designera", jeśli otworzy się okienko projektowania UI wraz z paletą kontrolek to znaczy, że do tej pory nie popełniliśmy błędu, w przeciwnym razie warto sprawdzić czy zostały dodane wszystkie wymagane biblioteki. Kolejnym krokiem jest utworzenie bean'a - będzie to kultowa klasa "Person". Wiem, wiem - w innych tutorialach mamy przycisk i pole tekstowe. W tym jednak potrzebna jest nam tabelka i przycisk - przyda się do kolejnego wpisu ;) Wróćmy jednak do tworzenia klasy danych, która powinna wyglądać mniej więcej tak :

package tutek.client.bean;

public class Person
{
 public String name; 
 public String surName;

 public Person(String name, String surName)
 {
  super();
  this.name = name;
  this.surName = surName;
 }
}

    Wracamy do tworzenia widoku. Dużą zaletą UIBindera jest deklaratywny sposób tworzenia interfejsu z użyciem XML, co doskonale wpasowuje się we wzorzec MVP. Poniżej XML do widoku, wystarczy go skopiować do własnego projektu :

<!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" xmlns:p1="urn:import:com.google.gwt.user.cellview.client">
 <ui:style>
  
 </ui:style>
 <g:HTMLPanel>
  <g:VerticalPanel>
   <p1:CellTable ui:field="cellTable"/>
   <g:FlowPanel>
    <g:Button ui:field="btn_filter" text="Filtruj Wacków"/>
   </g:FlowPanel>
  </g:VerticalPanel>
 </g:HTMLPanel>
</ui:UiBinder> 

 Jednak UIBinder to nie tlyko XML, towarzyszy mu klasa rozszerzająca "com.google.gwt.user.client.ui.Composite", podobnie jak XML, jest ona generowana podczas pracy z designerem. Jednak wymaga zmian - należy właściwie sparametryzować tabelkę wcześniej stworzoną klasą "Person".


package tutek.client.view;

import tutek.client.bean.Person;

import com.google.gwt.core.client.GWT;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.Widget;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.user.cellview.client.CellTable;
import com.google.gwt.user.client.ui.Button;

public class TutekView extends Composite
{

 private static TutekViewUiBinder uiBinder = GWT.create(TutekViewUiBinder.class);
 
 @UiField(provided=true) 
 public CellTable<Person> cellTable = new CellTable<Person>();
 @UiField 
 public Button btn_filter;

 interface TutekViewUiBinder extends UiBinder<Widget, TutekView>
 {
 }

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

}

Ok, mamy już widok, brakuje teraz prezentera - czegoś co by kontrolowało widok zgodnie z ideą wzorca. Jednak zanim stworzymy klasę rozszerzającą "com.mvp4g.client.presenter.BasePresenter" musimy się zatroszczyć o EventBus - oś wokół której obraca się cały framework MVP4G. Tworzymy zatem interfejs rozszerzający "com.mvp4g.client.event.EventBus":

package tutek.client;

import com.mvp4g.client.event.EventBus;



public interface TutekEventBus extends EventBus
{
    //pusty interfejs!!
}

Teraz gdy w projekcie mamy obecny eventBus można stworzyć klasę prezentera kontrolującą widok, w skład którego wchodzi tabelka i przycisk. Presenter będzie wypełniał tabelkę danymi oraz definiował obsługę zdarzenia kliknięcia na przycisku.

package tutek.client.presenter;


import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import tutek.client.TutekEventBus;
import tutek.client.bean.Person;
import tutek.client.view.TutekView;
import com.google.gwt.core.client.GWT;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.user.cellview.client.TextColumn;
import com.google.gwt.view.client.ListDataProvider;
import com.mvp4g.client.annotation.Presenter;
import com.mvp4g.client.presenter.BasePresenter;



@Presenter(view = TutekView.class)
public class TutekPresenter extends BasePresenter<TutekView, TutekEventBus>
{
 private ListDataProvider<Person> dataProvider;

Teraz czas na omówienie poszczgólnych fragmentów powyższego kodu. Najpierw za pomocą @Presenter określamy widok który będzie kontrolowany następnie deklarując klasę parametryzujemy ją eventBusem stworzonym w poprzednim kroku oraz widokiem. Następnie przeciążamy metodę "bind" :

 @Override
 public void bind()
 {
  GWT.log("bind");
  //tworzymy kolumny
  TextColumn<Person> name = new TextColumn<Person>() 
  {   
   @Override
   public String getValue(Person object)
   {
    return object.name;
   }
  };
  
  TextColumn<Person> surName = new TextColumn<Person>() 
  {   
   @Override
   public String getValue(Person object)
   {
    return object.surName;
   }
  };
  
  //dodajemy kolumny
  view.cellTable.addColumn(name);
  view.cellTable.addColumn(surName);
  
  //obsługa zdarzeń
  view.btn_filter.addClickHandler(new ClickHandler() 
  {   
   @Override
   public void onClick(ClickEvent event)
   {
    List<Person> lst = dataProvider.getList();    
    List<Person> newLst = new ArrayList<Person>();
    for(Person prs : lst)
    {
     if(prs.name.toLowerCase().equals("wacek"))
     {
      newLst.add(prs);
     }
    }
    dataProvider.setList(newLst);
   }
  });  
 }
Metoda "bind" jest wywoływana przy pierwszym odwołaniu do prezentera - jest to dobre miejsce na zainicjowanie kontrolek. Najpierw tworzymy kolumny dla tabeli, dodajemy je oraz na koniec definiujemy obsługę zdarzeń - przefiltrowanie wszystkich Wacków, mało eleganckie - ale zaradzimy temu w kolejnym poście ;). Pora wrócić do EventBus i uzupełnić interfejs o anotacje oraz zdarzenia :

package tutek.client;

import tutek.client.presenter.TutekPresenter;
import com.mvp4g.client.annotation.Event;
import com.mvp4g.client.annotation.Events;
import com.mvp4g.client.annotation.Start;
import com.mvp4g.client.event.EventBus;


@Events(startPresenter = TutekPresenter.class)
public interface TutekEventBus extends EventBus
{
 
 @Start //obowiązkowe zdarzenie startowe
 @Event(handlers = {TutekPresenter.class})
 public void onStart();

}

Trochę kodu przybyło. Pierwszą sprawą jest określenie inicjalnego prezentera - stanu początkowego. Następnie definiujemy zdarzenia zachodzące w aplikacji. Zdarzenie to nic innego jak metoda interfejsu. Wśród zdarzeń musi być wyszczególnione anotacją @Start zdarzenie startowe - łatwo zapamiętać- tylko jedno zdarzenie może posiadać tą anotację, która z koleji musi wystąpić. Dla każdego zdarzenia obowiązkowa jest anotacja @Event i to ona stanowi, że metoda interfejsu jest zdarzeniem. W parametrach tej anotacji określamy obsługujące prezentery - w tym przypadku tylko jeden, w którym musi być metoda obsługująca to zdarzenie o nazwie onOnStart(). Zasada jest prosta, trochę jak z java beans, do nazwy metody z iterfejsu EventBus dodajemy przedrostek "on" a pierwszą literę pierwotnej nazwy zmieniamy na wielką. Pora wrócić do prezentera i zobaczyć jak wygląda obsługa zdarzenia :

 public void onOnStart()
 {
  GWT.log("onStart()");
  //tworzymy dane
  Person persons[] = new Person[]{new Person("Wacek", "Kowalski"), new Person("Wacek", "Siczek"), new Person("Marcel", "Plusk"),
   new Person("Bronek", "Ogonek"), new Person("Wacek", "wachowski")};
  
  dataProvider = new ListDataProvider<Person>();
  dataProvider.addDataDisplay(view.cellTable);
     
  dataProvider.setList( new ArrayList<Person>(Arrays.asList(persons) ) );
 } 
}

Wywołanie metody "onOnStart" następuje po wygenerowaniu zdarzenia - odpowiada za to anotacja @Start, następuje po wywołaniu metody "bind" podczas inicjalizacji prezentera. Pozostałe zdarzenia są inicjowane w prezentererze za pomocą wywołania eventBus.<zdarzenie> - w tym momencie jest generowane zdarzenie, trafia na szynę gdzie rozjeżdża (obrazowo ;)) je prezenter zadeklarowany w parametrze "handlers" anotacji @Event . Wracając do naszego zdarzenia, jako że jest to zdarzenie generowane automatycznie w ramach inicjalizacji całej aplikacji, wypełniamy tabelkę danymi.

To już wszystko. Pozostaje jedynie uruchomić projekt. Jak widać korzystanie z MVP4G nie jest ani trudne, ani nie generuje kodu tempie wykładniczym - wręcz przeciwnie. Gotowy projekt Eclipsa również umieściłem   w moim magazynku plików do ściągnięcia. Następny wpis będzie rozwinięciem tego co teraz zostało zrobione.

2 komentarze:

  1. Super wpis, tak trzymać, moim zdaniem byłoby juz zupełnie perfekcyjnie gdyby było po eng ;).
    Proponuje również (tylko drobna sugestia) zeby zamiast 'magazynku plików' używać github'a lub bitbucket'a.
    Edit:
    Albo się coś zmieniło na bloggerze albo nie moge wybrać opcji 'Name'/'URL', IMHO tez by sie przydala :)

    OdpowiedzUsuń
  2. Dzięki :)

    pewnie z czasem stworzę bloga po angielsku gdzie przepiszę artykuły. Jeśli chodzi o magazynek plików - rzeczywiście, bitbucket wygląda lepiej :) dzięki za tipsa ;)

    OdpowiedzUsuń