|
|||
TradingActivityDemo. (NetBeans)TradingActivityDemo (NetBeans) Этап 1
1. Устанавливаем и настраиваем необходимое ПО (некоторые детали – в файле TradingActivityDemo_Deployment. doc, этап 1). 2. Создаем БД MySql с именем «trading_activity» из файла сценария “trading_activity_1. sql”, добавляем в таблицы тестовые данные; 3. Создаем первый набор проектов для формирования основного серверного приложения (создается как один проект типа Enterprise application без сборщика проектов Maven – вместо него по умолчанию будет использоваться Ant). EJB-составляющая этого приложения впоследствии будет работать с БД, предоставлять к ней доступ веб- и настольным клиентским приложениям. WEB-составляющая будет выводить публичную часть информации на веб-страницах, которые можно открыть в браузере с любого компьютера, подключенного к локальной сети предприятия и/или к Интернету; 4. В ejb-проекте генерируем классы сущностей на основе БД и фасады к ним (пример есть в каталоге articles, файл «Начало работы с приложениями Java EE. htm», раздел «Создание класса сущностей и фасада сеанса», а также здесь: http: //onedeveloper. ru/article? id=6). Соединение с БД, созданное в среде разработки, служит только для предоставления доступа фреймворку EclipseLink, который используя JPA, автоматически создаст сущности. Для работы самого приложения с БД нужно создать на сервере приложений пул соединений и привязать его к jndi-имени (пункты 8 и 9 из файла TradingActivityDemo_Deployment. doc). Перед началом генерирования сущностей EclipseLink создаст файл персистентности, где будет указано некоторое jndi-имя. В дальнейшем его нужно будет вручную заменить тем именем, которое мы создали на сервере приложений. Инсталлировать, запускать и останавливать сервер приложений, а также вызывать его консоль можно из среды разработки (на панели главного меню: сервис – серверы или в колонке с вкладками «проекты» и «файлы» вкладка «службы» - серверы); 5. Следующий проект будет предназначен для создания библиотеки интерфейса, связывающего основное серверное приложение со специальным настольным приложением-клиентом. Для его создания выполняем в среде разработки: создать проект – Java – Библиотека классов. 6. В ejb-модуль добавляем Session Bean, при создании в мастере выбираем удаленный интерфейс по имени проекта библиотеки классов. В проекте библиотеки появится интерфейс, а в классе Session Bean ejb-проекта – его поддержка. 7. В Session Bean добавляем методы, а в интерфейсе в проекте библиотеки появятся объявления этих методов (либо их нужно будет прописать там вручную); 8. Создаем третий проект – клиентское приложение, сначала оно будет консольным (тип при создании – Enterprise Application Client). В его библиотеки нужно добавить собранный проект библиотеки. Проект может не собираться, если файл подключаемой библиотеки находится в файловой системе по пути, содержащим пробелы. 9. В третьем проекте использовать удаленный метод, например:
//клиент InitialContext initialContext = new InitialContext(); RemoteServiceRemote remoteServiceRemote = (RemoteServiceRemote) initialContext. lookup(" remote_service" ); System. out. println(((Sale)remoteServiceRemote. getAllSales(). get(0)). getSecurityName());
10. В Session Bean внедряем фасад доступа к информации о продажах и реализовываем метод, возвращающий список объектов «продажа», содержащий всю информацию из соответствующей таблицы в реляционной БД:
//удаленный сервис в сессионном компоненте @EJB SaleFacade saleFacade;
@Override public List getAllSales() { return saleFacade. findAll(); }
//описатель развертывания GlassFish (если отсутствует – создать glassfish-ejb-jar. xml //в ejb-модуле) < enterprise-beans> < ejb> < ejb-name> RemoteService< /ejb-name> < jndi-name> remote_service< /jndi-name> < pass-by-reference> true< /pass-by-reference> < /ejb> < /enterprise-beans>
11. В проекте библиотеки классов:
//интерфейс для клиента в библиотеке классов @Remote public interface RemoteServiceRemote { List getAllSales(); }
Этап 2
12. В web-разделе Enterprise-решения добавляем фейслеты. Сначала создаем в папке «Веб-страницы» два подкаталога - для шаблонов и чанков, и для страниц. Добавляем главный шаблон, внутри тега < h: body> в теги ui: insert делаем вставки ui: include – ссылаемся на будущие чанки:
< div id=" top" class=" top" > < ui: insert name=" top" > < ui: include src=" header_chunk. xhtml" /> < /ui: insert> < /div> < div id=" left" class=" left" > < ui: insert name=" left" > < ui: include src=" left_menu_chunk. xhtml" /> < /ui: insert> < /div> < div id=" content" class=" right_content" > < ui: insert name=" content" > < ui: include src=" index_content_chunk. xhtml" /> < /ui: insert> < /div>
13. Создаем чанки с соответствующими именами. Создаем страницу на базе шаблона, при необходимости внутри тега ui: composition можно будет переопределять нужные чанки при помощи тега ui: define. В основном описателе развертывания должна быть информация о рендере фейслетов, например:
< context-param> < param-name> javax. faces. PROJECT_STAGE< /param-name> < param-value> Development< /param-value> < /context-param> < servlet> < servlet-name> Faces Servlet< /servlet-name> < servlet-class> javax. faces. webapp. FacesServlet< /servlet-class> < load-on-startup> 1< /load-on-startup> < /servlet> < servlet-mapping> < servlet-name> Faces Servlet< /servlet-name> < url-pattern> *. xhtml< /url-pattern> < /servlet-mapping> < welcome-file-list> < welcome-file> pages/index. xhtml< /welcome-file> < /welcome-file-list>
14. Добавляем файлы для локализации. Создаем пакет nls, добавляем в него «прочее-файл свойств» (. properties), добавляем в файл ключи и значения на разных языках (в пакет перед этим нужно добавлять файлы локализации для необходимых языков). Создаем faces-config. xml, добавляем в него ключи для переводов:
< application> < resource-bundle> < base-name> org. tyaa. tradingactivity. nls. messages< /base-name> < var> msg< /var> < /resource-bundle> < /application>
15. На страницах jsf (xhtml) через jsp-вызовы получаем локализованные значения строк (index_title – ключ одной из локализованных строк):
#{msg. index_title}
16. В веб-разделе создаем пакет bean, создаем в нем управляемый JSF-бин с областью видимости «приложение» (будет создан один экземпляр, общий для всех клиентов). Внедряем в него фасад доступа к данным о продажах и создаем метод получения заполненной данными коллекции (вызывать этот метод будет сам контейнер при попытке вывести данные на страницу):
@ManagedBean(name=" index_data" ) @ApplicationScoped public class IndexJSFManagedBean {
public IndexJSFManagedBean() {}
@EJB SaleFacade saleFacade;
public List getAllSales() { return saleFacade. findAll(); } }
17. На странице (чанк для вывода основного контента) подключаем все необходимые библиотеки и выводим содержимое коллекции в виде ссылок с параметром id (в БД значение цены хранится в целочисленном формате, а при выводе на страницу делится на 100):
< h: dataTable value=" #{index_data. allSales}" var=" sale" > < h: column> < f: facet name=" header" > id< /f: facet> < h: outputText value=" #{sale. id}" /> < /h: column> < h: column> < f: facet name=" header" > security name< /f: facet> < h: link value=" #{sale. securityName}" > < f: param name=" sale_id" value=" #{sale. id}" /> < /h: link> < /h: column> < h: column> < f: facet name=" header" > quantity< /f: facet> < h: outputText value=" #{sale. quantity}" /> < /h: column> < h: column> < f: facet name=" header" > price< /f: facet> < h: outputText value=" #{sale. price / 100}" /> < /h: column> < /h: dataTable>
18. До этого момента наше распределенное приложение только выводило информацию (в консоль и в окно браузера), которую заранее нужно было добавить в БД. Теперь доработаем удаленный консольный клиент так, чтобы при запуске он добавлял одну продажу в БД:
//в проекте интерфейса void addSale(Sale _sale, int _brokerId);
//в сессионном компоненте удаленного сервиса @EJB BrokerFacade brokerFacade; @Override public void addSale(Sale _sale, int _brokerId) { Broker broker = brokerFacade. find(_brokerId); if(broker! = null) { _sale. setBrokerId(broker); saleFacade. create(_sale); } }
//в проекте клиента (идентификатор брокера пока установим жестко равным 1) Sale newSale = new Sale(); newSale. setPrice(4100); newSale. setQuantity(200); newSale. setSecurityName(" ORCL" ); remoteServiceRemote. addSale(newSale, 1);
Этап 3
19. Дорабатываем структуру БД: добавляем таблицу «категории», в таблице «продажи» добавляем внешний ключ для связи с ней. Обновляем сущности на основе БД и фасады к ним (прежним способом – при помощи мастера EclipseLink). 20. Добавляем чанк левого меню, делаем в нем вывод списка категорий в виде ссылок с параметром – идентификатором категории:
< h: dataTable value=" #{index_data. allCategories}" var=" category" > < h: column> < h: link value=" #{category. name}" > < f: param name=" category_id" value=" #{category. id}" /> < /h: link> < /h: column> < /h: dataTable>
21. Корректируем стили страниц: добавляем в каталог ресурсов файл custom. css, в нем определяем / переопределяем нужные стили. Например, в стилях по умолчанию указан красный цвет для текста заголовка первого уровня, заменим его более «спокойным» цветом:
h1{ color: #dddddd; }
22. Соответствующим образом дополняем управляемый бин – поставщик данных для страниц:
@EJB CategoryFacade categoryFacade;
public List getAllCategories(){ return categoryFacade. findAll(); }
23. Подключаем чанк к шаблону, а на странице, использующей шаблон, временно переопределяем область основного контента (выводим в синхронном режиме значение идентификатора категории, переданное в парамете запроса):
#{param. category_id}
24. Добавляем к аннотации «управляемый бин» параметр – загружать данные еще до первого обращения за ними: (eager = true) 25. В чанке левого меню можно было бы заменить тег Link на CommandLink с указанием атрибута action, тогда в управляемом бине-поставщике данных для страниц можно было бы написать отдельное действие. В данном примере используется ссылка без команды в паре с уже существующим действием, в которое добавлен условный оператор. 26. В управляемом бине добавить развилку в действие наполнения списка продаж, а в файле представления главной страницы убрать временное переопределение области основного контента:
public List getSales(){ Map< String, String> params = FacesContext. getCurrentInstance() . getExternalContext() . getRequestParameterMap(); if(params. containsKey(" category_id" )){ Category category = categoryFacade. find(Integer. valueOf(params. get(" category_id" ))); return (List)category. getSaleCollection(); } return saleFacade. findAll(); }
27. Задание: а) добавить страницу покупки акций. Она должна открываться по клику по ID продажи и отображать наименование, количество, цену, кнопку «Заказать» (кнопку не программировать); б) добавить на главную страницу строку поиска продаж акций по набору символов в названии фирмы, запрограммировать ее функционал.
Этап 4
28. Сделаем загрузку области основного контента асинхронной, при этом перед загрузкой будет отображаться сообщение. В web-проекте решения в файле шаблона главной страницы в теге < h: head> указываем подключения будущего файла js-сценариев: < h: outputScript library=" js" name=" custom. js" />. После блока, выводящего чанк хедера, добавляем блок сообщения о загрузке контента:
< div id=" loading_wrapper" > < p> Loading... < /p> < /div>
, а в фале стилей задаем ему невидимость:
#loading_wrapper{ width: 100%; display: block; text-align: center; position: fixed; display: none; }
#loading_wrapper p{ background-color: #fde88e; width: 100px; height: 20px; margin: 0 auto; font-weight: bold; }
29. В каталоге resources создаем подкаталог js, помещаем в него файл сценариев для веб-клиента custom. js. Добавляем в него функцию отображения и скрытия сообщения «Загрузка …»:
function showProgress(data) { if (data. status === " begin" ) { document. getElementById('loading_wrapper'). style. display = " block"; } else if (data. status === " success" ) { document. getElementById('loading_wrapper'). style. display = " none"; } }
30. В чанке области основного контента тегу dataTable зададим значение атрибута id, например, salesList. По этому идентификатору будет определяться область, которую нужно обновлять асинхронно. 31. В чанке левого меню содержимое тега < ui: composition> обернем в тег < h: form>. Тег формирования ссылок должен быть h: commandLink. В нем, после строки формирования параметра запроса, добавляем элемент асинхронного режима выполнения запроса (будет выполняться по событию «click», запускать функцию «showProgress» из файла js-сценариев, отправлять запрос методу «getSales()», находящемуся в управляемом бине, на основании ответа обновлять область разметки, помеченную атрибутом «salesList», где префикс «двоеточие» обозначает, что данный идентификатор следует искать вне формы, в которой расположен сам элемент «f: ajax»):
< f: ajax event=" click" onevent=" showProgress" render=": salesList" listener=" #{index_data. getSales()}" />
32. Если сообщение о загрузке исчезает слишком быстро, для отладки допускается приостанавливать поток, в котором выполняется управляемый бин (первой строкой в теле соответствующего метода прописать «Thread. sleep(1000); »); 33. Чтобы в ответ на ajax-запросы выводилась актуальная информация (например, веб-страница была открыта или перезагружена, затем в БД добавились новые записи о продажах, а после пользователь кликнул на названии категории), нужно в файл фасада добавить код принудительной очистки кэша. В качестве примера в файле AbstractFacade в шаблонный метод find первой строкой добавляем вызов очистки кэша:
getEntityManager() . getEntityManagerFactory() . getCache() . evict(entityClass, id);
34. Задание: а) сделать асинхронным обновление области основного контента при отображении результатов поиска продаж, функционал которого был осуществлен в предыдущем задании. б) Каким образом на производительность и надежность системы повлияет очистка кэша, добавленная нами в AbstractFacade? Если негативно, то как это исправить?
Этап 5
35. Настроим переключение локалей веб-клиента, заготовленных ранее. В каталог «resources» добавляем подкаталог «images», в него – файлы картинок для выбора локалей. 36. Создаем в пакете «bean» управляемый компонент переключения локалей:
@ManagedBean(name=" locale_changer", eager = true) @SessionScoped public class LocaleChangerManagedBean { private Locale currentLocale = FacesContext. getCurrentInstance(). getViewRoot(). getLocale(); public LocaleChangerManagedBean() {} public void changeLocale(String localeCode) { currentLocale = new Locale(localeCode); } public Locale getCurrentLocale() { return currentLocale; } }
37. Создаем чанк для вывода иконок выбора локали, содержащий:
< div class=" locales" > < h: form> < h: commandLink action=" #{locale_changer. changeLocale('en')}" > < h: graphicImage library=" images" name=" en-us_round_icon_64.png" title=" #{msg. english}" /> < /h: commandLink> < h: commandLink action = " #{locale_changer. changeLocale('ru')}" > < h: graphicImage library=" images" name=" ru-ru_round_icon_64.png" title=" #{msg. russian}" /> < /h: commandLink> < /h: form> < /div>
38. Подключаем этот чанк в чанк «шапки» сайта:
< ui: include src=" /templates/locales_chunk. xhtml" />
39. Для проверки работы переключения локалей, кроме тайтла страниц подставляем вывод локализованных строк в теле страниц, например, в заголовке колонки категорий вместо статического текста подставляем #{msg. categories_title} (предварительно добавив соответствующую строку в файлы локалей). Весь код в шаблоне главной страницы нужно обернуть в тег < f: view locale=" #{locale_changer. currentLocale}" > (внутри должны оказаться теги h: head и h: body). 40. Если локали не подключились автоматически, следует в описателе развертывания «faces-config» в тег < application> добавить указание локали по умолчанию и других поддерживаемых локалей:
< locale-config> < default-locale> ru< /default-locale> < /locale-config> < supported-locale> en< /supported-locale>
41. Задание: проверить корректность механизма локализации при работе дополнительных элементов пользовательского интерфейса (например, поисковой строки). В случае выявления багов исправить их.
Этап 6 Форматируем вывод чисел на веб-страницу Обрабатываем ошибки AJAX при помощи OmniFaces Отправляем и обрабатываем сообщения JMS Устанавливаем задачу на серверной стороне при помощи EJB Schedule
42. Корректируем формат вывода чисел в области основного контента веб-представления:
< h: outputText value=" #{sale. price/100}" > < f: convertNumber pattern=" #00. 00" /> < /h: outputText>
43. Для вывода таблицы, сформированной тегами div, можно использовать универсальный тег циклического вывода jsf (фрагмент кода):
< ui: repeat value=" #{main_data. allSales}" var=" sale" > < div class=" row" > < div class=" col" > < h: outputText value=" #{sale. id}" /> < /div>
44. Основа стиля для нового отображения таблицы:
. table{display: table; } . row{display: table-row; } . col{display: table-cell; padding-right: 5px; } . col-header{display: table-cell; padding-right: 5px; font-weight: 600; }
45. Сейчас при возможных ошибках AJAX-запросов получаются непредвиденные результаты. Чтобы обрабатывать эти ошибки, добавляем в web-проект решения библиотеку OmniFaces. В файле faces-config. xml объявляем фабрику обработчиков исключений AJAX-запросов, и в ней – необходимый тип обработчика (внутри раздела < application> ):
< factory> < exception-handler-factory> org. omnifaces. exceptionhandler. FullAjaxExceptionHandlerFactory< /exception-handler-factory> < /factory>
46. В каталоге WEB-INF создаем файл фейслета, в контенте которого выводим информацию об исключении. 47. В файле web. xml внутри раздела < web-app> добавляем связку «тип исключения – страница ошибки» (для развертывания не подходит версия GlassFish4. 0):
< error-page> < exception-type> javax. faces. application. ViewExpiredException< /exception-type> < location> /WEB-INF/errorpages/expired. xhtml< /location> < /error-page>
48. Для возможности асинхронного оповещения сервисов о добавлении информации в БД используем JMS. Создаем на сервере приложений в разделе JMS Resources фабрику соединений с именем JNDI (например, jms/TradingActivityPool) и точку назначения типа «топик» (jms/TradingActivityWebTopic). 49. В EJB-проекте решения в файле RemoteService внедряем оба ресурса:
@Resource(name=" jms/TradingActivityPool" ) private ConnectionFactory connectionFactory;
@Resource(name=" jms/TradingActivityWebTopic" ) private Destination destination;
50. Там же создаем метод отправки сообщений:
public void sendActionString(String _actionString) { try { Connection connection = connectionFactory. createConnection(); Session session = connection. createSession(true, Session. AUTO_ACKNOWLEDGE); MessageProducer producer = session. createProducer(destination); TextMessage message = session. createTextMessage(); message. setStringProperty(" message_type", " action" ); message. setText(_actionString); producer. send(message); System. out. println(" message sent" ); session. close(); connection. close(); } catch (JMSException ex) { System. err. println(" Sending message error" ); ex. printStackTrace(); } }
51. Там же вызываем его в нужный момент и передаем в аргумент текст сообщения; 52. В WEB-проекте добавляем в каталог bean управляемый сообщениями бин:
@MessageDriven( mappedName=" jms/TradingActivityWebTopic", name = " IndexMDB" ) public class IndexMessageDrivenBean implements MessageListener{
@Override public void onMessage(Message msg) { try { TextMessage message = (TextMessage)msg; //считываем свойство из соответствующего поля, заданное вручную в consumer System. out. println(" FROM MDB - message type IS " + message. getStringProperty(" message_type" )); //считываем само сообщение System. out. println(" FROM MDB - payload IS " + message. getText()); } catch (JMSException ex) { ex. printStackTrace(); } }
}
53. Запускаем удаленного настольного клиента, в консоли сервера приложений смотрим вывод при отправке и получении сообщения. Вместо отладочного вывода в дальнейшем можно добавить логику. 54. В WEB-проекте в бин, управляемый фейслетом, добавляем планирование циклического выполнения задачи – обновления текущей веб-страницы каждую минуту:
@Schedule(minute=" */1" ) public void updateJsfPage() throws IOException { System. out. println(" Jsf page updating" ); FacesContext. getCurrentInstance(). getExternalContext() . redirect(" #" ); }
55. Задание: добавить еще одного получателя сообщений из уже существующего «топика». Добавить еще одну цепочку передачи сообщений JMS, но вместо «топик» использовать тип точки назначения «очередь», и принимать сообщения одним получателем. Этап 7 Переводим клиент Enterprise-приложения с консольного на JavaFX
56. В классе Main настольного клиента добавляем extends Application, переопределяем метод start, комментируем содержимое метода main(), вместо него добавляем метод запуска JavaFX-приложения: launch(args); 57. Добавляем пакет с двумя файлами ScreensFramework от Angela Caicedo (прилагаются). 58. Создаем пакет для представлений, создаем в нем два пустых текстовых файла с расширением fxml – главный и добавления продажи. Открываем каждый из них в приложении SceneBuilder2. 0, добавляем по корневому элементу (тип - AnchorPane). В NetBeans кликаем ПКМ по каждому из файлов представлений, выбираем пункт «установить контроллер». Контроллеры выносим в отдельный пакет. В файлах представлений в открывающей тег элемента AnchorPane добавляем атрибут fx: controller с указанием полностью квалифицированного имени соответствующего контроллера (имя_пакета. ИмяКласса). 59. В классах контроллеров устанавливаем поддержку интерфейса ControlledScreen из ScreensFarmework, реализовываем метод setScreenParent, в его тело добавляем установку родительского контейнера JavaFX, а также добавляем соответствующее поле класса:
ScreensController myController; //… @Override public void setScreenParent(ScreensController screenParent) { myController = screenParent; }
60. В классе Main создаем поля, отражающие информацию о всех экранах, которые можно будет загрузить в окно приложения (сейчас их два):
public static String mainID = " main"; public static String mainView = " /org/tyaa/taclient/view/Main. fxml"; public static String addSaleID = " add_sale"; public static String addSaleView = " /org/tyaa/taclient/view/AddSale. fxml";
61. Там же в теле метода start добавляем два экрана, устанавливаем экран по умолчанию, создаем пустой корневой элемент, добавляем в него контейнер сменных экранов. На базе корневого элемента создаем одну сцену, на ее базе – один стейдж, отображаем его:
//Создаем объект скринс-фреймворка (контейнер представлений) ScreensController screensContainer = new ScreensController(); //Добавляем в него представления главного окна и окна добавления продажи screensContainer. loadScreen(Main. mainID, Main. mainView); screensContainer. loadScreen(Main. addSaleID, Main. addSaleView); //Устанавливаем представление главного окна в качестве текуего screensContainer. setScreen(Main. mainID); //Создаем корневой контейнер, помещаем в него наш контейнер представлений, //на его базе - сцену, которую подключаем в главный стейдж и отображаем стейдж Group root = new Group(); root. getChildren(). addAll(screensContainer); Scene scene = new Scene(root); primaryStage. setScene(scene); primaryStage. show();
62. Добавляем в контроллеры методы обработки событий для будущих контролов переключения между экранами, например:
@FXML private void goToAddSaleScreen(ActionEvent event){ myController. setScreen(Main. addSaleID); }
63. В разметке представлений добавляем необходимые импорты и реализуем внутри корневого элемента графический интерфейс переключения между экранами с привязкой к обработчикам в контролере, например:
<? import java. lang. *? > <? import javafx. scene. layout. *? > <? import java. util. *? > <? import javafx. geometry. *? > <? import javafx. scene. *? > <? import javafx. scene. control. *? > <? import javafx. scene. text. *? > //… < children> < Label text=" My sales" > < font> < Font size=" 18. 0" /> < /font> < /Label> < Button mnemonicParsing=" false" onAction=" #goToAddSaleScreen" text=" Add sale" /> < /children>
64. Эту же разметку открываем в SceneBuilder, в корневой элемент добавляем VBox, в него – три панели AnchorPane. В самую верхнюю из трех панелей добавляем заголовок и название технологии в виде элементов Label, которым в разделе настроек Layout (правая колонка в SceneBuilder) задаем привязки к сторонам родительского AnchorPane. В следующую панель добавляем название таблицы и переносим туда же кнопку перехода на экран добавления продажи. На третью панель добавляем элемент TableView. 65. Переходим в код представления и заменяем в разметке таблицы колонки, сгенерированные автоматически, на необходимые нам:
< columns> < TableColumn fx: id=" idTableColumn" sortable=" true" text=" id" /> < TableColumn fx: id=" securityNameTableColumn" sortable=" true" text=" security name" /> < TableColumn fx: id=" quantityTableColumn" sortable=" true" text=" quantity" /> < TableColumn fx: id=" priceTableColumn" sortable=" true" text=" price" /> < /columns>
66. Создаем пакет для ресурсов, а в нем – файл каскадной таблицы стилей. Добавляем в него свойства для стандартного класса панелей:
. pane{-fx-background-color: lightgrey; }
67. В SceneBuilder в разделе Properties для родительского AnchorPane устанавливаем stylesheet – выбираем созданный нами css, а в поле Style Class выбираем pane. 68. В коде стилей для метки-заголовка создаем пользовательский класс:
. caption{-fx-text-fill: lightslategrey; }
69. В SceneBuilder в поле Style Class выбираем caption (можно также в исходном коде добавлять нужным тегам атрибут styleClass=" имя_класса_стилей" ). Предпросмотр результат можно сделать в SceneBuilder: Preview -> Show preview in window. 70. Создаем пакет model, в нем – новый класс SaleModel. Он будет предоставлять модель данных продажи для отображения в таблице JavaFX TableView, в отличие от класса модели Sale, являющегося сущностью для обмена данными с реляционной БД. Строим его как класс модели со свойствами:
private IntegerProperty id; private StringProperty securityName; private IntegerProperty quantity; private DoubleProperty price;
public SaleModel(int _id, String _securityName, int _quantity, double _price){ this. id = new SimpleIntegerProperty(_id); this. securityName = new SimpleStringProperty(_securityName); this. quantity = new SimpleIntegerProperty(_quantity); this. price = new SimpleDoubleProperty(_price); }
public int getId() {return id. getValue(); } public IntegerProperty idProperty() {return id; } //…
71. В контроллере главного экрана добавляем поля для внедрения графических элементов из разметки и поля для коллекции данных, поставляемых таблице:
@FXML private TableView mySalesTableView; @FXML private TableColumn idTableColumn; //… ObservableList< SaleModel> sales;
72. Там же, в теле метода initialize, при помощи удаленного сервиса получаем список продаж, в цикле заполняем наблюдабельную коллекцию данными из него, к элементам-колонкам таблицы подключаем соответствующие свойства модели продаж, к элементу-таблице подключаем заполненную наблюдабельную коллекцию:
sales = FXCollections. observableArrayList(); InitialContext initialContext = null; RemoteServiceRemote remoteServiceRemote = null; initialContext = new InitialContext(); remoteServiceRemote = (RemoteServiceRemote) initialContext. lookup(" remote_service" ); List allMySales = remoteServiceRemote. getAllSales(); for (Object object: allMySales) { Sale sale = (Sale)object; sales. add(new SaleModel( sale. getId() , sale. getSecurityName() , sale. getQuantity() , sale. getPrice()) ); } idTableColumn. setCellValueFactory( new PropertyValueFactory< SaleModel, String> (" id" ) ); securityNameTableColumn. setCellValueFactory( new PropertyValueFactory< SaleModel, String> (" securityName" ) ); quantityTableColumn. setCellValueFactory( new PropertyValueFactory< SaleModel, String> (" quantity" ) ); priceTableColumn. setCellValueFactory( new PropertyValueFactory< SaleModel, String> (" price" ) ); mySalesTableView. setItems(sales);
73. Цены в колонке price выводятся как есть (целые числа из БД). Во время вывода в представление их значения нужно делить на 100 и форматировать в соответствии с локалью «en-US». Для этого сначала сделаем вывод значения цены в виде типа Double вместо String:
priceTableColumn. setCellValueFactory( new PropertyValueFactory< SaleModel, Double> (" price" ) );
74. Затем подготовим форматирующий объект, настроив его на нужную локаль, а также – на нужное число знаков до и после запятой:
NumberFormat doubleToCurrencyFormatter = NumberFormat. getCurrencyInstance(new Locale(" en", " US" )); doubleToCurrencyFormatter. setMinimumIntegerDigits(1); doubleToCurrencyFormatter. setMinimumFractionDigits(2);
75. Остается задать объекту колонки price измененную фабрику ячеек, внутри которой будем делить значение цены на 100, а затем применять метод format форматирующего объекта:
priceTableColumn. setCellFactory(column -> { return new TableCell< SaleModel, Double> (){ @Override protected void updateItem(Double item, boolean empty) { super. updateItem(item, empty); if (item == null || empty) { setText(null); setStyle(" " ); } else { setText(doubleToCurrencyFormatter. format(item / 100)); } }
}; });
76. Теперь нужно создать форму для ввода параметров продажи и ее отправки в БД. Открываем SceneBuilder, вверху в левой колонке жмем на кнопку настроек (маленькая шестеренка), выбираем импорт JAR. Нужно подключить файл библиотеки controlsfx, который скачивается с одноименного сайта. Этот же файл нужно добавить в набор библиотек в проекте десктопного клиента. 77. Контроллеру второго экрана нужно предоставить объект для выполнения удаленных методов при помощи RMI (можно скопировать код из контроллера первого экрана и вынести в отдельный файл, или временно – в файл второго контроллера). 78. Открываем в SceneBuilder разметку второго представления, добавляем VBox, в него – три AnchorPane. В первую панель добавляем Label – заголовок формы («Добавить продажу»), во вторую – VBox с вложенными HBox, в каждом из которых – пара Label и CustomTextField. В третью панель добавляем HBox с двумя Button – «Добавить» и «Назад». 79. Всем элементам настраиваем расположение. Каждому полю ввода задаем fx: id, например, securityNameCTextField. Можно также задать Prompt Text. Кнопке «Добавить» устанавливаем обработчик события OnAction, например, actionAddSale, а также в разметке добавляем атрибут defaultButton=”true”. Кнопке отмены в разметке добавляем атрибут cancelButton. 80. В контроллере второго экрана внедряем поля ввода:
@FXML CustomTextField securityNameCTextField;
81. Там же создаем переменную для валидатора:
ValidationSupport validationSupport;
82. Включаем экстрактор для нестандартного типа контрола, чтобы валидатор реагировал на события с этим типом контролов:
ValueExtractor. addObservableValueExtractor( c -> c instanceof CustomTextField , c -> ((CustomTextField) c). textProperty() );
83. Инициализируем и настраиваем валидатор для всех полей:
validationSupport = new ValidationSupport(); validationSupport. setErrorDecorationEnabled(true); validationSupport. registerValidator( securityNameCTextField , Validator. createEmptyValidator(" Security name is required" ) ); //…
84. Создаем и заполняем метод (действие) обработки событий кнопки «Добавить»:
public void actionAddSale(ActionEvent actionEvent) { //securityNameCTextField. getStyleClass(). remove(" error-c-text-field" ); //securityNameCTextField. getStyleClass(). add(" custom-text-field" ); List< ValidationMessage> validationMessageList = (List< ValidationMessage> ) validationSupport . getValidationResult() . getMessages(); if (validationMessageList. isEmpty()) { securityNameCTextField. promptTextProperty() . setValue(" security name" ); //... securityNameCTextField. setStyle(" -fx-border-color: lightgray; " ); //... Sale newSale = new Sale(); newSale. setPrice((int)(Double. parseDouble(priceCTextField. getText()) * 100)); newSale. setQuantity(Integer. parseInt(quantityCTextField. getText())); newSale. setSecurityName(securityNameCTextField. getText()); remoteServiceRemote. addSale(newSale, 1, 1); securityNameCTextField. textProperty(). setValue(" " ); //... } else { String currentControlIdString; for (ValidationMessage validationMessage: validationMessageList) { currentControlIdString = ((CustomTextField)validationMessage. getTarget()). getId(); switch(currentControlIdString){ case " securityNameCTextField" : { securityNameCTextField. promptTextProperty() . setValue(validationMessage. getText()); //securityNameCTextField. getStyleClass(). remove(" custom-text-field" ); //securityNameCTextField. getStyleClass(). add(" error-c-text-field" ); securityNameCTextField. setStyle(" -fx-border-color: red; " ); break; } //... } } } }
85. Задание: а) во время запуска приложения показывать заставку («пожалуйста, подождите, идет загрузка данных с сервера…»); б) переработать удаленный метод добавления продажи так, чтобы он возвращал некоторое значение, получив которое клиентское приложение сообщало бы об успешном или неуспешном добавлении данных в БД, а во время добавления показывало соответствующее сообщение; в) добавить в первое представление кнопку «обновить» и запрограммировать ее, чтобы по клику обновлялись данные в таблице; г) найти необходимую документацию и советы, и с их помощью выяснить: как вместо RMI применять очередь JMS для связи серверной и клиентской сторон, как это сделать для обычного клиентского приложения, можно ли каким-то способом применять для такого клиента RMI.
|
|||
|