27 April 2010

Применение bind в JavaFX

Lumber room
Когда я впервые познакомился с техникой связывания переменных, то в первое время хотелось связывать всё подряд, настолько это было увлекательно. Как и любую технологию, JavaFX и binding не следует применять бездумно. Следует помнить, что binding в сущности спрятанная реализация паттерна Observer (или Listeners, кому как больше нравится). Как следствие, может возникать множество не очевидных проблем, таких как «утечки памяти», проблемы с производительностью и т.п.

В этом посте хотелось бы привести ряд паттернов и антипаттернов применения binding'а в JavaFX. Кроме того, второй задачей является опубликовать ответы на некоторые вопросы, которые часто задавали на Sun Tech Days, когда я «дежурил» на стенде JavaFX. Мне кажется, что многие подобные вопросы плохо освещены, и в рунете особенно.

Итак, за дело. Предполагается что читатель имеет хоть какое-то представление о JavaFX. Однако, на случай, если читатель знает лишь понаслышке о bind, приведу простой пример его использования:
	value : String = "Hello, World";
	def boundValue : String = bind "The title is: {value}";

	FX.println("{boundValue}");

	value = "Yet another value";

	FX.println("{boundValue}");


Ключевое слово bind во второй строчке говорит, что значение переменной boundVariable всегда равно результату вычисления следующего за ключевым словом выражения («The title is: {value}»).

Если запустить приложение, то получим:
	cy6ergn0m@cgmachine ~/test/fx $ javafxc Main.fx
	cy6ergn0m@cgmachine ~/test/fx $ javafx Main    
	The title is: Hello, World
	The title is: Yet another value


Каким-бы образом ни было изменено значение переменной value, переменная boundVariable обновляется автоматически (за счёт спрятанного внутри JavaFX runtime кода, регистрирующего слушателя (listener) на переменную value).

Выражение, используемое при связывании может быть как простым (один к одному), так и сложным: можно вызывать функции и арифметические операции.

Следует отметить, что если в выражении bind вызывается функция, то зависимости вычисляются таким образом, что переменная пересчитывается только если меняются переменные, участвующие в самом выражении, а не те, которые используются внутри используемой функции.

Например:
	var a : Integer = 1;
	var b : Integer = 2;

	function test(value : Integer) : Integer {
	      value + b
	}

	def boundVariable : Integer = bind test(a);

	FX.println( "bound: {boundVariable}" );

	a = 2;

	FX.println( "bound: {boundVariable}" );

	b = 3;

	FX.println( "bound: {boundVariable}" );


Выполняем и видим:
	cy6ergn0m@cgmachine ~/test/fx $ javafxc Main2.fx 
	cy6ergn0m@cgmachine ~/test/fx $ javafx Main2
	bound: 3
	bound: 4
	bound: 4
	


Видно, что когда мы обновили переменную «a», то boundVariable была пересчитана, а когда обновили «b», то ничего не произошло, так как «b» не используется в выражении bind.

JavaFX воспринимает функцию, участвующую в выражении как «чёрный ящик» и не пытается учитывать возможные побочные эффекты (side effects) как со сторону самой функции, так и со стороны остального кода (за исключением, самого выражения в bind конечно).

Это, казалось бы, неполноценное поведение можно использовать на пользу. Таким образом можно защититься от чрезмерно частого пересчёта переменной (полезно в сложных случаях, когда вычисляемое выражение создаёт сложные и тяжеловесные объекты). Часто оказывается, что мы на самом деле не хотим пересчитывать всё выражение, если изменяются ЛЮБЫЕ величины, участвующие в нём.

В таких случаях, такие «пассивные» части можно вынести внутрь отдельной функции. Кроме того, это может помочь избежать каскадных обновлений, когда многие переменные друг от друга зависят и начинают обновляться по многу раз. Иногда количество пересчётов становится столь велико, а создаваемые объекты настолько тяжеловесны, то производительность приложения может сильно пострадать.

Следует также отметить, что КАЖОЕ изменение переменной, с которой связаны какие-то другие, приводит к немедленному пернсчёту всех зависимых переменных. Так что операция присвоения (скрытый вызов метода set) не завершится до тех пор, пока все зависимости не будут обновлены. Если от зависимых переменных зависят какие-то другие, то они тоже будут пересчитаны и т.д. рекурсивно будут обновляться все переменные. При этом можно получить циклическое «бесконечное» обновление (оно конечно конечное, так как завершится из-за переполнени стэка).

На стенде задавали вопросы о возможности динамического создания объектов\компонентов в приложении. Во многих случаях, можно осуществить связывание элементов управления с моделью данных напрямую с помощью bind. Тогда при изменениях в модели данных приложения, отображение (UI) будет обновляться автоматически.

Например,
	import javafx.scene.Scene;
	import javafx.stage.Stage;
	import javafx.scene.control.Button;
	import javafx.scene.Group;

	var itemsList : String[] = [ "One", "Two", "Three" ];

	function loadItems() : Void {
	    // do interaction with server-side, read from file, etc..
	    itemsList = 
	        for( i in [1..10] )
	                "element {i}";
	}


	Stage {
	    width: 200
	    height: 400
	    scene: Scene {
	        content: [
	            Button {
	                text: "Load..."
	                action: loadItems
	            }
	            Group {
	                content: bind
	                    for( item in itemsList )
	                        Button {
	                            text: "{item}"
	                            height: 20
	                            translateX: 10
	                            translateY: 28 + 22 * indexof item
	                        }
	            }
	        ]
	    }
	}
	


Если запустим приложение, то увидим окно. Список кнопок под кнопкой «Load» связаны со списком «itemsList», и всегда когда мы обновляем его (список itemsList), то и состав кнопок также меняется. Таким образом, мы связали отображение (кнопки) с моделью данных приложения (в данном случае просто список строк). Кроме того, здесь мы применили связывание не просто значений, а связали последовательности (Sequence, список в терминологии JavaFX) со списками.

вид приложения после нажатия кнопки Load

Следует заметить, что при пересчёте выражения для Group.content все кнопки будут пересозданы, а старые экземпляры кнопок будут уничтожены. Так что если у приложения сложная и большая сцена, то не стоит связывать её всю целиком с моделью данных, так как частые обновления модели приведут к частому пересозданию всех компонентов\объектов сцены, что приведёт к проблемам с производительностью. В то же время, такое связывание чрезвычайно удобно и надёжно и гораздо меньше вероятность ошибок.

Логику функции loadItems можно было бы реализовать как-нибудь поинтереснее, например, можно было бы вызвать веб-сервис и получить данные с сервера.

Помимо прочего, связывание работает не только если мы переназначаем всю последовательность, но и если мы изменяется лишь один из элементов последовательности.
Например, если мы заменим код функции loadItems следующим:
	function loadItems() : Void {
	    // do interaction with server-side, read from file, etc..
	    itemsList[1] = "Hey, Iam here!";
	}


То при нажатии на кнопку Load список кнопок ниже также будет пересоздан и наше изменение вступит в силу.

Среди прочего, меня также спрашивали о возможности работы bind в JavaFX при использовании с Java. Ответ в данном случае просто: если из Java обновлять поле JavaFX, то все bind'ы отработают, так как из Java мы вызываем метод set для этого поля, который сгенерирован JavaFX компилятором. Этот метод в свою очередь сделает всё необходимое при обновлении значения поля.

Однако, в некоторых случаях возникает необходимость в обратном: нужно связать JavaFX переменную с переменной в Java. В таком случае, придётся делать это вручную следующим образом:
— создать Java-интерфейс слушателя
— создать на JavaFX своего рода адаптер Java-JavaFX, который бы реализовывал этот интерфейс и содержал public-read поле, а некий метод добавлял бы его в список слушателей Java-кода;
— когда в Java-коде что-то изменяется, то Java-код вызывает всех слушателей, в том числе и наш JavaFX-слушатель, который обновит своё public-read поле.

На это самое public-read поле и можно уже завязываться из остального JavaFX-кода.

Пример:

Интерфейс слушателя (Listener.java):
public interface Listener {
	void notifyChanged( int newValue );
}


Java-код (JavaPart.java):
public class JavaPart {
	private int observerableValue;

	public void setObserverableValue(int newValue) {
		observerableValue = newValue;

		Listener l = listener;
		if(l != null)
			l.notifyChanged(newValue);

	}

	public int getObserverableValue() {
		return observerableValue;
	}

	private Listener listener;

	public void setListener(Listener l) {
		listener = l;
	}

}


JavaFX-адептер (JavaPartAdapter.fx):
public class JavaPartAdapter extends Listener {

	public-init var javaPart : JavaPart;

	init {
		javaPart.setListener(this);
	}

	public-read var currentValue : Integer;

	public override function notifyChanged( newValue : Integer ) : Void {
		currentValue = newValue;
	}

}


И JavaFX код, который использует классы выше чтобы связать свою переменную с переменной из Java-класса (Main.fx):
import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.scene.control.Button;
import javafx.scene.Group;

def javaPart : JavaPart = new JavaPart();
def adapter : JavaPartAdapter = JavaPartAdapter {
	javaPart: javaPart;
};

Stage {
    width: 200
    height: 300
    scene: Scene {
	content: [
	    Button {
		text: bind "Change {adapter.currentValue}"
		action: function() {
			javaPart.setObserverableValue(77);
		}
	    }
	]
    }
}


Вот так это выглядит при выполнении. Слева — до нажатия на кнопку, а справа — после. Как видно, вызов метода setObserverableValue привёл к вызву слушателя (JavaPartAdapter), который обновил переменную currentValue, на которую мы и ссылаемся в UI.

ДоПосле

Хотя, на первый взгляд, методика и выглядит несколько громоздкой, однако она несложная по своей сущности и надёжно работает. Таким образом, можно считать, что «приличный» способ связывания JavaFX переменных с Java-переменными существует.

Не следует также упускать из вида следующую потенциальную опасность. В JavaFX возможно получить «утечку памяти» за счёт невнимательного обращения с bind. Как уже ранее отмечалось, bind на самом деле неявно создаёт слушателя, т.е. возникает обратная ссылка. Если ссылки своевременно не обнулять, то слушатели могут стать причиной того, что неиспользуемые объекты всё ещё будут достижимы, так что они не будут уничтожены в ходе сборки мусора. При таком раскладе можно получить Out Of Memory.

Например:
class A {
        var a : Integer;
}

class B {
        var b : Integer;
}

def b : B = B {};

for( i in [1..100000] ) {
        var a : A = A {
                a: bind b.b
        };
}


Здесь, мы неявно передаём ссылки на создаваемые экземпляры класса A в переменную b класса B. У переменной b образуется длинный длинный список зависимых от неё переменных, таким образом, экземпляры класса A не могут быть выброшены в ходе сборки мусора, так как они всё ещё достижимы.

cy6ergn0m@cgmachine test/fx/oom $ javafx -Xmx16m Main
java.lang.OutOfMemoryError: Java heap space
        at Main.javafx$run$(Main.fx:11)
cy6ergn0m@cgmachine test/fx/oom $ 


Но стоит закомментировать строку a: bind b.b, как программы выполняется успешно, так как все созданные экземпляры класса A могут быть легко освобождены.

Если вместо этого написать что-то вроде такого:
class A {
        var a : Integer;
}

class B {
        var b : Integer;
}

var b : B = B {};

for( i in [1..100000] ) {
        A {
                a: bind b.b
        };
        b = B {};

}


То это выполнится, хотя и не на ура (довольно медленно).
К сожалению, в общем случае способа снять bind нет. Однако, во многих случаях можно найти рабочий workaround, сущность которого будет зависеть уже от конкретики. В общем случае, можно дать совет не забывать, что bind добавляет слушателя (a), так чтобы при изменении значения (b.b) можно было найти всех зависимых (все экземпляры A в моём примере) и уведомить их о необходимости пересчитать своё значение (a.a).

На этом мой рассказ о применении bind в JavaFX подходит к концу. Снимаю шляпу перед теми отважными храбрецами, что дочитали это до конца и желаю удачи в освоении этой замечательной по сути технологии, JavaFX.
Tags:javafxriadevelopmentbindbinding
Hubs: Lumber room
+4
2.7k 3
Comments 1
Popular right now
Data Analyst
December 8, 2020102,000 ₽SkillFactory
Python для анализа данных
December 9, 202024,900 ₽SkillFactory
Профессия Data Scientist
December 9, 2020162,000 ₽SkillFactory
Специализация Data Science
December 9, 2020114,000 ₽SkillFactory
Машинное обучение
December 11, 202049,000 ₽Нетология
Top of the last 24 hours