Języki

Erudis - your road to knowledge
Akcje – obsługa zdarzeń

Wreszcie dochodzimy do najciekawszego elementu SAF, uproszczonego definiowania akcji. W Swingu jeśli chcemy, żeby jakiś komponent oprócz siedzenia w oknie aplikacji mógł zrobić coś pożytecznego, musieliśmy dodać do niego nasłuchiwacza zdarzeń (ActionListener lub innego), a następnie tegoż nasłuchiwacza zaimplementować jako osobną klasę.

SAF całą tę procedurę bardzo upraszcza, po prostu tworzymy metodę, oznaczamy ją metadaną @Action i dodajemy do mapy akcji informację o tym, że pojawiła się obsługa pewnego zdarzenia.

Popatrzmy na fragment kodu, wzięty z klasy MainPane (Listing 3).

Listing 3. Demonstracja obsługi zdarzenia polegającego na naciśnięciu przycisku Szukaj w oknie aplikacji

1. //fragmenty klasy MainPane  
2. public class MainPane extends javax.swing.JPanel {  
3.   
4.   File f = null;  
5.   
6.   public MainPane() {  
7.     initComponents();  
8.   }  
9.   
10.   //w metodzie pozostawiona jest tylko inicjalizacja
        //interesujących nas w tej chwili komponentów  
11.   private void initComponents() {  
12.     jTextArea1 = new javax.swing.JTextArea();  
13.     jButton1 = new javax.swing.JButton();  
14.     jTextField2 = new javax.swing.JTextField();  
15.   
16.     jTextArea1.setColumns(20);  
17.     jTextArea1.setEditable(false);  
18.     jTextArea1.setFont(resourceMap.getFont("jTextArea1.font"));  
19.     jTextArea1.setRows(5);  
20.     jTextArea1.setName("jTextArea1");  
21.   
22.     ActionMap actionMap = Application.getInstance(MainApp.class).getContext().
              getActionMap(MainPane.class, this);  
23.     jButton1.setAction(actionMap.get("search"));  
24.     jButton1.setText(resourceMap.getString("jButton1.text"));  
25.     jButton1.setName("jButton1");   
26.   
27.     jTextField1.setEditable(false);  
28.     jTextField1.setText(resourceMap.getString("jTextField1.text"));  
29.     jTextField1.setName("jTextField1");  
30.   }  
31.   
32.   @Action  
33.   public void search() {  
34.     jTextArea1.setText("");  
35.     Grep g = new Grep(jTextField2.getText());  
36.     try {  
37.       if(f != null)  
38.         g.searchInFiles(f);  
39.     } catch (FileNotFoundException ex) {  
40.       Logger.getLogger(MainPane.class.getName()).log(Level.SEVERE, null, ex);  
41.     } catch (IOException ex) {  
42.       Logger.getLogger(MainPane.class.getName()).log(Level.SEVERE, null, ex);  
43.     }  
44.     getJTextArea1().setText(g.getFormatedSearchResults());  
45.   }  
46. }      

Interesuje nas wyłącznie obsługa przycisku Szukaj, dlatego cały kod służący do innych celów został usunięty z Listingu 3. W linii 32 zaczyna się metoda search, oznaczamy ją metadaną @Action, żeby zarejestrować ją jako akcję. Pozostaje jeszcze powiązać odpowiedni przycisk (JButton1) z tę akcją. Robimy to w linii 23, wykorzystując klasę javax.swing.ActionMap.

Warto zwrócić uwagę na to, że możemy się odwoływać do akcji zdefiniowanych w innych klasach, przekazując odpowiedni obiekt do metody ApplicationContext.getActionMap(Class, Object) – jest to bardzo wygodne, gdyż raz zdefiniowaną akcję można wykorzystywać w całej aplikacji. Pewnie najwygodniejszy byłby tutaj mechanizm wstrzykiwania zależności (ang. dependency injection), ale SAF ma być prosty, więc przynajmniej w obecnej wersji wyszukujemy odpowiednią klasę i metodę z kontekstu aplikacji.

Opisana implementacja funkcjonalności wyszukiwania jest bardzo prosta, ma jednakże jedną dosyć zasadniczą wadę: jeżeli wyszukiwanie trwa dłużej, to blokuje ono interfejs użytkownika, w szczególności nie ma możliwości przerwania wyszukiwania. Dlaczego tak się dzieje jest jasne – długotrwałe zadanie wykonujemy w wątku EDT, odpowiedzialnym za rysowanie interfejsu użytkownika.

Musimy zatem uruchomić wyszukiwanie w osobnym wątku. Brzmi to z pozoru dość prosto, ale w praktyce jest uciążliwe, gdyż trzeba zsynchronizować działanie wątku szukającego z wątkiem EDT, rysującym interfejs użytkownika. Dotychczas najwygodniejszym rozwiązaniem było wykorzystanie klasy narzędziowej SwingWorker. Podobne podejście stosuje SAF, tyle, że użycie analogicznego mechanizmu jest prostsze niż w przypadku SwingWorker-a.

Przyjrzymy się jeszcze raz klasie MainPane, tyle, że innemu jej fragmentowi, który znajduje się na Listingu 4.

Listing 4. Implementacja wyszukiwania zrobiona w ten sposób, by odbywało się ono w osobnym wątku

1. //fragmenty klasy MainPane  
2. public class MainPane extends javax.swing.JPanel {  
3.   
4.   File f = null;  
5.   
6.   public MainPane() {  
7.     initComponents();  
8.   }  
9.    //w metodzie pozostawiona jest tylko inicjalizacja interesujących nas w tej chwili komponentów  
10.   private void initComponents() {  
11.   ActionMap actionMap = Application.getInstance(MainApp.class).
            getContext().getActionMap(MainPane.class, this);  
12.   
13.     jButton2.setAction(actionMap.get("searchNoBlocking"));  
14.     jButton2.setText(resourceMap.getString("jButton2.text"));  
15.     jButton2.setName("jButton2");  
16.   
17.     jTextField2.setText(resourceMap.getString("jTextField2.text"));  
18.     jTextField2.setName("jTextField2");  
19.   
20.     jButton4.setAction(actionMap.get("cancel"));  
21.     jButton4.setText(resourceMap.getString("jButton4.text"));  
22.     jButton4.setName("jButton4");  
23.   }  
24.   
25.   @Action(block=Task.BlockingScope.COMPONENT)  
26.   public Task searchNoBlocking() {  
27.     searchTask = new SearchNoBlockingTask(Application.getInstance(MainApp.class));  
28.     return searchTask;  
29.   }  
30.   
31.   private class SearchNoBlockingTask extends Task

Tak jak wcześniej, do komponentu, w tym przypadku przycisku, przypisujemy akcję searchNoBlocking (linia 13). Metoda obsługująca wyszukiwanie zwraca obiekt typu Task, którego instancję musimy utworzyć w metodzie searchNoBlocking (linia 27).

Oczywiście nikt nam nie da gotowej klasy Task – trzeba ją samodzielnie napisać, przesłaniając odpowiednie metody. Nasza implementacja klasy Task nazywa się SearchNoBlockingTask (linia 31), przesłaniamy w niej trzy metody:

  • konstruktor, w którym inicjalizujemy potrzebne obiekty, warto zwrócić uwagę na to, że możemy w nim modyfikować stan GUI;
  • doInBackground(), która jest odpowiedzialna za uruchomienie wyszukiwania – ta metoda działa w osobnym wątku, nie blokuje zatem interfejsu użytkownika, nie wolno w tej metodzie odwoływać się w związku z tym do elementów GUI. Metoda ta jest automatycznie uruchomiana, gdy tworzymy instancję klasy SearchNoBlockingTask;
  • succeeded, która jest wywoływana gdy tylko skończy się działanie metody doInBackground() – w tej metodzie możemy zaktualizować interfejs użytkownika przy pomocy danych uzyskanych z metody doInBackground(). Metoda ta jest oczywiście uruchomiona w wątku EDT, dlatego może modyfikować GUI.

Klasa SearchNoBlockingTask ma jeszcze inne użyteczne metody, z jednej z nich korzystamy w metodzie cancel, umieszczonej na samym końcu Listingu 4, przerywa ona wyszukiwanie po kliknięciu w odpowiedni przycisk.