Вероятно, есть немного энтузиастов разработчиков Eclipse, которые не могут оценить усердные усилия Ральфа Штернберга и его команды по улучшению пользовательского интерфейса приложений Eclipse RCP. Даже с готовящимся проектом E4 Rich Client Tooling постепенная замена всего набора инструментов виджета Eclipse 3 SWT еще далека, и понимание RAP для создания замены HTML / Javascript для них стало настоящим гением!
Тем не менее, есть несколько фундаментальных проблем, когда вы хотите сделать браузерный вариант платформы с расширенным клиентом, и одна из наиболее заметных заключается в том, что запрос на основе браузера всегда начинается с события, порожденного в самом браузере, такого как щелкнув ссылку или нажав кнопку. Пока приложение уважает эту логику, беспокоиться не о чем; приложение знает, что что-то изменится (например, заполнить таблицу значениями или изменить настройки в поле со списком), и может предвидеть это. Большинство действий RCP, которые влияют на пользовательский интерфейс, действительно работают таким образом, и обработка события пользовательского интерфейса может быть выполнена путем обработки события выбора или его эквивалента.
Проблемы начинают возникать, когда вы не хотите ждать результатов, а передаете обработку запроса в отдельный поток. Когда результаты возвращаются, пользовательский интерфейс должен быть уведомлен о том, что он может обновить виджеты, поскольку фактическое обновление должно выполняться в основном потоке пользовательского интерфейса. Для этого Display.asynexec()
обычно нормально работает в обычных приложениях RCP, но в RAP это не гарантия, и вы можете получить результаты, которые не отображаются на экране. или UIThread
исключения. Эта проблема усугубляется, когда результаты должны быть представлены в пользовательском интерфейсе, которые никогда не были вызваны действием пользовательского интерфейса!
Давайте возьмем пример из реальной жизни , что мы столкнулись в программе Aquabots , что мы работаем в научно — исследовательском центре устойчивого PortCities в Rotterdam Университете прикладных наук . В рамках этой программы студенты строят небольшие автономные суда для проверки и мониторинга. В настоящее время мы можем отправлять траектории путевых точек на эти суда, после чего они будут следовать желаемому курсу с GPS-координатами. Клиент, который мы разработали, был основан на Eclipse RAP:
Для карты мы используем OpenLayer 3 , библиотеку javascript, такую как Google Maps, которая содержит большую часть функций для построения траектории, увеличения и уменьшения масштаба и отображения соответствующих данных на графике. В RAP библиотека может быть легко интегрирована в RCP с помощью Browser
виджета и настройки:
browser.setUrl( INDEX_HTML );
INDEX_HTML
указывает на ресурс, который был добавлен в реестр расширений (файл plugins.xml) и содержит код, необходимый для создания OpenLayers 3
карты.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="chrome=1">
<meta name="viewport" content="initial-scale=1.0, user-scalable=no, width=device-width">
<style>
.map {
width: 100%;
}
</style>
<script src="js/jquery-2.1.3.min.js" type="text/javascript"></script>
<script src="http://openlayers.org/en/v3.8.2/build/ol.js" type="text/javascript"></script>
<script src="js/openlayer.js" type="text/javascript"></script>
<script src="js/general.js" type="text/javascript"></script>
<link rel="stylesheet" href="http://ol3js.org/en/master/css/ol.css" type="text/css">
<link rel="stylesheet" href="theme/maps.css" type="text/css">
<title>Aquabots Dashboard</title>
</head>
<body onload='initInteraction()'>
<div id="map"></div>
</body>
</html>
Фрагмент 1: Index.html
Все события, порожденные из пользовательского интерфейса, передаются OpenLayer 3
через
BrowserUtil.evaluate(browser, <my_js_string> <my_callback_function> );
Таким образом, функциональность, предоставляемая OpenLayer 3
RAP, может быть вызвана. Все идет нормально!
Однако это не дает нам возможности реагировать на результаты, которые возвращаются OpenLayer 3
, и на сосуды, которые покачиваются на воде! Эти данные предлагаются нескольким сервлетам, которые также зарегистрированы в реестре расширений, но теперь нам нужно отобразить эти данные в виджетах, и именно здесь вступает в действие механизм PushSession .
Push-сессия
Напомним, что теперь мы имеем дело с событиями, порожденными javascript и сервлетами, с которыми пользовательский интерфейс не знает, как и когда они происходят. Несомненно, есть кнопка, которая была нажата в какой-то момент, ссылка, по которой щелкали, но нет никакого способа сообщить, когда базовая модель и виджеты обновлены, и пользовательский интерфейс может быть обновлен. О да, и есть вероятность, что может быть возвращено больше событий, которые могут быть запущены из пользовательского интерфейса, но продолжают поступать впоследствии, например, когда собираются данные мониторинга.
Ребята из RAP придумали довольно изящное решение этой проблемы, а именно push-сессию . Хитрость заключается в том, чтобы эмулировать стандартный подход на основе браузера, запуская push-сессиюиз пользовательского интерфейса, который обрабатывается после обновления модели представления. Сразу после этого активируется следующий push-сеанс в ожидании другого возможного события. Поэтому push-сеанс не отвечает на события, которые должны произойти, как обычное событие пользовательского интерфейса, вместо этого он ожидает возможное событие и обрабатывает его, если оно происходит.
Рисунок 2: Диаграмма связи
Механизм в общих чертах показан на рисунке 2. В некотором смысле, он следует регулярному шаблону обновления пользовательского интерфейса после изменения модели, но с добавлением запуска еще одного push-сеанса для другого события. Это продолжается до тех пор, пока пользовательский интерфейс не будет удален.
Другая дополнительная проблема заключается в том, что обновление пользовательского интерфейса все еще должно выполняться в основном потоке пользовательского интерфейса, поэтому в какой-то момент мы должны включить хорошо известный Display.asyncexec()
механизм. Для этого я создал абстрактный класс, который обрабатывает большинство проблем с потоками, которые вы можете увидеть во фрагменте 2:
import java.util.ArrayList;
import java.util.Collection;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.eclipse.rap.rwt.service.ServerPushSession;
import org.eclipse.swt.widgets.Display;
public abstract class AbstractPushSession {
public static final int DEFAULT_TIMEOUT = 5000;
private Display display;
private ServerPushSession session;
private ExecutorService es;
private boolean refresh = false;
private int timeout;
private Collection<ISessionListener> listeners;
protected AbstractPushSession() {
this( DEFAULT_TIMEOUT );
}
protected AbstractPushSession( int timeout ) {
this.timeout = timeout;
listeners = new ArrayList<ISessionListener>();
es = Executors.newCachedThreadPool();
}
private Runnable runnable = new Runnable() {
public void run() {
while(!refresh && !runSession()){
try{
Thread.sleep( timeout );
}
catch( InterruptedException ex ){
ex.printStackTrace();
}
}
if( display.isDisposed())
return;
display.asyncExec( new Runnable() {
public void run() {
for(ISessionListener listener: listeners)
listener.notifySessionChanged( new SessionEvent( this ));
session.stop();
start();
}
});
};
};
public void addSessionListener( ISessionListener listener ){
this.listeners.add( listener );
}
public void removeSessionListener( ISessionListener listener ){
this.listeners.remove( listener );
}
public void init( Display display ){
this.display = display;
}
protected boolean hasRefreshed() {
return refresh;
}
protected void setRefresh(boolean refresh) {
this.refresh = refresh;
}
public synchronized void start(){
session = new ServerPushSession();
session.start();
this.refresh = false;
es.execute(runnable);
}
/**
* Run the session. returns true if the session completed succesfully.
* @return
*/
protected abstract boolean runSession();
public synchronized void stop(){
this.listeners.clear();
this.refresh = false;
es.shutdown();
}
}
Фрагмент 2: основной механизм Push-сессии
Абстрактный класс представляет слушателя сеанса, который позаботится о пользовательском интерфейсе после обновления модели. Пользовательский интерфейс создает экземпляр сеанса push, обеспечивает его отображение, запускает его в первый раз и останавливает, когда пользовательский интерфейс удаляется.
public class MapSession extends AbstractPushSession {
private Model model;
private IModelListener listener =
new IModelListener() {
@Override
public void notifyStatusChanged( ModelEvent event) {
setRefresh( true );
Thread.interrupted();
}
};
private static MapSession session =
new MapSession();
public static MapSession getInstance(){
return session;
}
public void setModel( Model model) {
this.model = model;
this.model.addListener(listener);
}
@Override
protected boolean runSession() {
return ( this.model != null );
}
@Override
public void stop() {
if( this.model != null )
this.model.removeListener(listener);
super.stop();
}
}
Фрагмент 3: Реализация механизма Push-сессии.
Реализация, изображенная выше, показывает, что сеанс фактически начинается, как только модель впервые добавляется в сеанс push; только тогда
start()
будет иметь эффект. В этом случае
MapSession
слушает изменения в модели, после чего
refresh
Команда дана. Это активирует
Runnable
поток в абстрактном классе, который следит за тем, чтобы все слушатели сеанса могли обновить свои виджеты в основном потоке пользовательского интерфейса.
Пример его использования в виджете изображен ниже:
public class MyComposite extends Composite {
private MapSession session =
MapSession.getInstance();
private ISessionListener sl =
new ISessionListener(){
@Override
public void notifySessionChanged(SessionEvent
event){
refresh();
}
};
public MyComposite(Composite parent, int style) {
super(parent, style);
...
this.session.addSessionListener(sl);
this.session.init( Display.getDefault());
this.session.start();
}
/**
* Refresh the UI
*/
public void refresh(){
...
}
@Override
public void dispose() {
session.stop();
super.dispose();
}
}
Фрагмент 4. Активация push-сессии в Composite
Композит следит за тем, чтобы был загружен правильный дисплей и начался сеанс push. Обработка изменений в модели оставлена на MapSession
усмотрение объекта. Обратите внимание, что это также может быть отложено до тех пор, пока не будет запущено первое событие пользовательского интерфейса, которое начинает связь.
Наконец, нам нужен объект, который запускается в отдельном потоке, например, сервлет.
public class ModelServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private Model model;
private MapSession session = MapSession.getInstance();
@Override
public void init() throws ServletException {
session.setModel(model);
super.init();
}
@Override
public void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
...
updateModel();
...
}
}
Фрагмент 5: отдельная нить.
Вывод
Опция Push Session достаточно стабильна и полезна, когда вы освоите ее работу. Приложив немного дополнительного кодирования, вы можете реализовать класс, который заботится о большей части стандартного кода, необходимого для посредничества между моделью и представлением при работе с асинхронной связью, а первый работает в отдельном потоке. Надеюсь, этот вклад делает это немного проще.