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 TaskTak 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:
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.