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