The service provided by Consileon was professional and comprehensive with a very good understanding of our needs and constrains.

Wolfgang Hafenmayer, Managing partner, LGT Venture Philanthropy

Technical quality of staff offered, capability of performing various project roles, as well as motivation and dedication to the project (... [...]

dr Walter Benzing, Head of development B2O, net mobile AG

Technical quality of staff offered, capability of performing roles of developers, lead developers, trainers, team leaders, architects as wel [...]

Karl Lohmann, Itellium Systems & Services GmbH

Firma Consileon Polska jest niezawodnym i godnym zaufania partnerem w biznesie, realizującym usługi z należytą starannością (...)

Waldemar Ściesiek, Dyrektor zarządzający IT, Polski Bank

The team was always highly motivated and professional in every aspect to perform on critical needs of our startup environment.

Denis Benic, Founder of Ink Labs

ZK – tworzenie aplikacji sterowanej zdarzeniami

Category: Other Tags: ,

Większość frameworków/bibliotek odpowiedzialnych za obsługę interfejsu użytkownika definiuje zdarzenia i dostarcza szereg mechanizmów służących do przechwytywania i przetwarzania zdarzeń. Komponenty GUI oferowane przez te biblioteki specyfikują jakie zdarzenia i kiedy są przez nie generowane oraz na jakie zdarzenia i w jakim celu nasłuchują. Zdarzenia to nie tylko akcje wykonane przez użytkownika takie jak kliknięcie myszką na przycisk, wybór elementu na liście czy naciśnięcie klawisza Enter w polu tekstowym. Komponenty używają bowiem zdarzeń również do komunikacji między sobą dzięki czemu powiązania między komponentami są luźne. Jest to jeden z ważnych czynników zwiększających elastyczność architektury systemu.

Dlaczego zatem nie wprowadzić zdarzeń do warstwy logiki biznesowej? Nie jest to nowa koncepcja, aczkolwiek rzadko spotykana w typowych aplikacjach biznesowych. Wynika to m.in. z braku odpowiedniego wsparcia ze strony frameworków aplikacyjnych. Z nadejściem JEE 6 sytuacja może się zmienić. Specyfikacja CDI definiuje prosty w użyciu model generowania i odbierania zdarzeń (podobny model od dawna oferuje Seam). Model ten jest uniwersalny i może mieć zastosowanie w różnych obszarach/warstwach aplikacji. W poniższym artykule zaprezentuję rozwiązanie jakie w tym zakresie oferuje framework ZK.

Przykładowa aplikacja

W typowej aplikacji biznesowej mamy zwykle do zaimplementowania obsługę grupy operacji określanej skrótem CRUD (tworzenie, zmiana, usuwanie) wykonywanych przez użytkownika na różnych obiektach biznesowych. W naszej przykładowej aplikacji zamodelujmy obsługę ról użytkowników. Mamy zatem ekran z listą ról oraz ekran edycji/tworzenia roli wyświetlany w oknie dialogowym. Rysunki poniżej.

Zdarzenie biznesowe

Zdarzenie w ZK definiowane jest jako obiekt klasy Event posiadający nazwę (name). Dodatkowo zdarzenie może mieć przypisany komponent docelowy (target) oraz dowolny obiekt z danymi (data).

Zdefiniujmy zatem zdarzenia biznesowe, które chcielibyśmy obsłużyć w naszej aplikacji:

  • onEdit – użytkownik wykonał akcję mającą na celu przejście do trybu edycji wybranego obiektu biznesowego

  • onAdd – użytkownik wykonał akcję mającą na celu przejście do trybu edycji nowego obiektu biznesowego

  • onDelete – użytkownik wykonał akcję mającą na celu usunięcie wybranego obiektu biznesowego

  • onSave – użytkownik wykonał akcję mającą na celu zapisanie zmian (modyfikacja bądź utworzenie obiektu biznesowego)

Przypisanie zdarzenia biznesowego do akcji użytkownika

W naszej aplikacji edycja następuje po kliknięciu na element listy, a tworzenie elementu po naciśnięciu przycisku znajdującego się pod listą. W obu przypadkach nastąpi wyświetlenie okna edycji (popup).

<window id="windowRoleList" apply="${roleListCtrl}">
  <listbox model="@{controller.listModel}" selectedItem="@{controller.selected}">
   <listitem self="@{each='role'}">
    <listcell>
      <a label="@{role.name}" forward="onClick=onEdit"/>
    </listcell>
    <listcell label="@{role.description}" />
   </listitem>
  </listbox>
  <toolbar>
   <button id="btnAdd" forward="onClick=onAdd"/>
   <button id="btnDelete" forward="onClick=onDelete"/>
  </toolbar>
 </window>

Zwróćmy uwagę na atrybuty forward zdefiniowane dla linku (element a w wierszu listy) i przycisku pod listą (element button). W atrybucie forward podajemy nazwę zdarzenia jakie zostanie propagowane w górę drzewa komponentów. Propagowanie odbywa się poprzez utworzenie nowego zdarzenia (o nazwie podanej po znaku równości) zawierającego zdarzenie źródłowe (o nazwie podanej przed znakiem równości). W naszym przypadku zdarzeniem źródłowym jest zdarzenie systemowe onClick (kliknięcie na przycisk/link). W przypadku nie podania zdarzenia źródłowego, przyjmowane jest domyślne, różne w zależności od komponentu dla jakiego specyfikujemy atrybut forward. W przypadku linka i przycisku jest to zdarzenie onClick. Zatem oba zapisy są tożsame:

<button forward="onClick=onAdd"/>
<button forward="onAdd"/>

Zdefiniowaliśmy zatem, po jakiej akcji użytkownika nastąpi określone zdarzenie biznesowe. Odseparowaliśmy tym samym logikę interfejsu użytkownika od logiki biznesowej. Gdybyśmy dla innego obiektu biznesowego edycję chcieli przeprowadzić nie w osobnym oknie, lecz w tym samym, w którym znajduje się lista wyboru, zdarzenie onEdit moglibyśmy zdefiniować jako następstwo wyboru elementu na liście:

<listbox model="@{controller.listModel}" selectedItem="@{controller.selected}" 
   forward="onSelect=onEdit"/>

W obu przypadkach logika obsługi zdarzenia onEdit będzie taka sama. Przyjrzyjmy się zatem jak obsłużyć zdarzenie w kodzie aplikacji.

Obsługa zdarzenia

Zdarzenie propagowane jest aż do komponentu okna (window) i następnie zostaje przekazane do kontrolera podpiętego pod to okno (kontroler definiujemy atrybutem apply). W naszym przypadku kontroler jest bean-em zarządzanym przez Springa o nazwie roleListCtrl (użycie Spring-a jest opcjonalne).

Kontroler musi zdefiniować metodę odpowiadającą nazwie zdarzenia:

public void onEdit(Event event);

Wiemy zatem, w jaki sposób nasze zdarzenie biznesowe może być utworzone i obsłużone w wyniku akcji podjętej przez użytkownika. Spyta ktoś, czym to rozwiązanie różni się od klasycznego podejścia używanego w innych frameworkach np. Seam, gdzie możemy analogicznie wywołać metodę w kontrolerze:

<button action="#{controller.onEdit()}"/>

Rozpatrzmy różnice między oboma podejściami z punktu widzenia interfejsu wywołania/obsługi komunikatu:

Seam: w metodzie możemy przekazywać dowolną ilość argumentów, dowolnego typu.

ZK: W zdarzeniu możemy przekazać jeden obiekt danych (możemy np. przekazać wybraną rolę: forward=onEdit(role)) . Typem danych jest zawsze Object. Porównując zatem z wywołaniem metody, mamy ograniczoną ilość argumentów i konieczność rzutowania typu po stronie kontrolera. Zwykle jednak nie ma konieczności przekazywania większej liczby argumentów (w naszym przykładzie nie musimy przekazywać roli w zdarzeniu, gdyż framework potrafi wstrzyknąć do kontrolera wybraną rolę automatycznie). Podkreślić należy, że oba problemy wynikają z ograniczeń jakie niesie ze sobą propagowanie zdarzeń przy przy pomocy atrybutu forward. Nie istnieje tutaj możliwość stworzenia własnej klasy zdarzenia. Gdy wysyłamy zdarzenie w kodzie aplikacji (przykład później),problemy powyższe nie istnieją.

Przejdźmy w końcu do przykładów, gdzie zdarzenia zaczynają pokazywać swoją moc 🙂

Zdarzenia w akcji

W przypadku gdy wyświetlamy kilka okien na stronie możemy używać zdarzeń do komunikacji między oknami sterując w ten sposób logiką aplikacji.

Dodajmy na naszej stronie z listą ról nowy panel z listą użytkowników. Lista ról niech wyświetla tylko role dla wybranego użytkownika (z możliwością ich edycji). Obie listy umieszczamy w oddzielnych oknach (komponentach window) dzięki czemu obie listy możemy obsługiwać oddzielnymi kontrolerami (w obu oknach chcemy obsłużyć logikę CRUD). Odświeżenie listy ról po wybraniu użytkownika obsługujemy następująco:

//Kontroler użytkowników

public void onSelect(Event event) {
 	//inform role list window on selection change
 	Component roleListWindow = self.getFellow("windowRoleList");
	Events.postEvent(new Event("onUserSelected", roleListWindow, getSelected()));
}
//Kontroler ról

public void onUserSelected(Event event) {
	selectedUser = (User)event.getData();
	refreshList();
}

Dzięki temu, że zdefiniowaliśmy zdarzenie onUserSelected, możemy zarejestrować słuchaczy obserwujących to zdarzenia w celu zaimplementowania dodatkowej logiki. Jako przykład wykorzystam wbudowany w ZK mechanizm sterowania bindowaniem danych. Co to jest bindowanie? Jest to możliwość bezpośredniego połączenia warstwy modelu (danych) z kontrolkami wyświetlającymi/modyfikującymi te dane. W ZK bindowanie może obejmować dowolne właściwości kontrolek np. stan kontrolki (czy kontrolka jest aktywna czy nie (enabled/disabled)). Dodajmy zatem wymaganie w naszej aplikacji aby usunięcie roli było możliwe tylko dla użytkownika nowo utworzonego (dla którego pole registered = false). Musimy zatem przy zmianie selekcji użytkownika deaktywować bądź aktywować przycisk Delete pod listą ról. Korzystając z możliwości mechanizmu bindowania ZK konfigurujemy właściwość disabled dla przycisku Delete:

<button id="btnDelete" forward="onDelete"
	    disabled="@{controller.selectedUser.registered, load-after='windowRoleList.onUserSelected'}"/>

Użycie @ w wyrażeniu oznacza zastosowanie bindingu. W naszym przypadku odczytujemy pole registered z obiektu użytkownika przekazanego uprzednio do kontrolera (patrz kod wyżej). Za pomocą parametru load-after, wskazujemy zdarzenie, po nastąpieniu którego nastąpi odświeżenie przycisku (odczytanie danych z modelu). Framework automatycznie zarejestruje odpowiedniego listenera w komponencie naszego okna nasłuchującego na zdarzenie onUserSelected.

Zauważmy co zyskujemy. Kod obsługujący zdarzenie onUserSelected jest czysty. Modyfikujemy w nim tylko stan modelu, czyli ustawiamy zmienną selectedUser i odświeżamy listę ról (model). Zarówno przy odświeżaniu listy ról jak i odświeżaniu przycisku na stronie zostanie odczytane pole selectedUser. Kontroler nie musi wiedzieć kiedy odświeżyć przycisk, nie musi nawet wiedzieć o jego istnieniu.

Zdarzenia globalne

Przedstawione dotychczas zdarzenia były wysyłane do konkretnego komponentu. Nie zawsze jest to pożądane. Jedną z podstawowych cech architektur sterowanych zdarzeniami jest możliwość niezależnego wysyłania i odbierania zdarzeń. Emitent zdarzenia nie musi znać odbiorców. Odbiorca nie musi wiedzieć skąd zdarzenie pochodzi. Jak tę funkcjonalność zrealizować w ZK pokaże znowu na konkretnym przykładzie. Dodajmy na górze naszej strony panel zawierający nazwę zalogowanego użytkownika. Pojawia się kwestia odświeżenia zawartości panelu w momencie modyfikacji nazwy aktualnie zalogowanego użytkownika. W kontrolerze obsługującym okno edycji użytkownika implementujemy obsługę zdarzenia onSave.

public void onSave(Event event) {
	//save user
	....
	//inform potential listeners
	EventQueue lookup = EventQueues.lookup("QUEUE_GLOBAL", true);
	lookup.publish(new UserChangedEvent("onUserChanged", user));
}

Wysyłamy zatem zdarzenie onUserChanged bez adresata do kolejki o zdefiniowanej przez nas nazwie QUEUE_GLOBAL. Zauważmy, że tym razem zdefiniowaliśmy własną klasę zdarzenia (UserChangedEvent), w której przekazujemy obiekt użytkownika. Standardowo, rejestracja w kontrolerze słuchacza zdarzeń z tej kolejki wygląda w następujący sposób:

//kontroler nagłówka
 public void onCreate(Event event) {
  EventQueues.lookup("QUEUE_GLOBAL", EventQueues.DESKTOP, true)
   .subscribe(new EventListener() {
    @Override
    public void onEvent(Event event) throws Exception {
     //process event 
    }
   });
 }

Rejestrację można uprościć i jednocześnie ułatwić kontrolerowi obsługę zdarzenia tworząc klasę pośrednicząca w odbieraniu i przekazywaniu zdarzeń do wybranego przez nas komponentu (nazwijmy ją EventsManager):

public class EventsManager {
 public static void subscribe(String queue, final Component component) {
  EventQueues.lookup(queue, EventQueues.DESKTOP, true)
   .subscribe(new EventListener() {
    @Override
    public void onEvent(Event event) throws Exception {
     Events.sendEvent(component, event);
    }
   });
 }
}

Teraz w kontrolerze nagłówka możemy w sposób standardowy zaimplementować obsługę zdarzenia onUserChanged.

// kontroler nagłówka

public void onCreate(Event event) {
  //register self (window component) as listener of events from QUEUE_GLOBAL
  EventsManager.subscribe("QUEUE_GLOBAL", self);
}

public void onUserChanged(UserChangedEvent event) {
  if (event.getUser().eqauls(getUserWorkspace().getLoggedInUser()) {
   //refresh component displaying user name
   ...   
  }
}

Używając zdarzeń globalnych uwalniamy emitenta zdarzenia od konieczności znajdowania komponentu docelowego. Jednocześnie uwalniamy odbiorcę od rejestracji nasłuchiwania w konkretnym komponencie. Wystarczy, że obie strony uzgodnią kanał (kolejkę) komunikacji. Zdarzenia globalne domyślnie wysyłane i odbierane są w kontekście strony przeglądarki (desktop-level). ZK pozwala również funkcjonować zdarzeniom w kontekście całej aplikacji. Dzięki temu strona może zostać odświeżona pomimo braku akcji ze strony zalogowanego użytkownika (np. na skutek operacji wykonanej przez scheduler-a uruchomionego na serwerze). Umożliwia to technologia Push Server, oferowana wewnątrz ZK.

http://pkaczor.blogspot.com/2010/10/zk-tworzenie-aplikacji-sterowanej.html


Paweł Kaczor

Programista, pasjonat.

Twórca Akka-DDD – frameworku do budowy skalowalnych systemów w architekturze DDD/CQRS/ES. Interesuje się programowaniem funkcyjnym. W wolnym czasie kraulista, szachista amator.

@PavelKaczor
pkaczor.blogspot.com
github.com/pawelkaczor


Tags: ,

Comments

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Trwa ładowanie