Adobe

индекс
116,75

Flex Data Binding Tricks

Меньше года назад меня вовлекли в проект, для которого необходимо было писать клиента на Flex. Так как я был новичком в этом деле, то в процессе работы я находил что-то новое и совершенно неизвестное мне. В то время я и открыл для себя Flex Data Binding (связыванием данных). Я думаю, что каждый, кто работает с Flex очень скоро сталкивается с Data Binding.
Связывание данных заключается в том, что мы можем с легкостью связывать два объекта (источника данных) между собой, что позволяет поддерживать их синхронизацию. Примером может служить связывание между собой элементов пользовательского интерфейса путем создания некоторых правил поведения, что способствует созданию более интерактивного пользовательского интерфейса.
Имея некоторый опыт в этой области, я решил разобрать все типы механизма связывания данных во Flex. Думаю эта статья будет интересна не только новичкам, но и профессионалы почерпнут из неё что-нибудь для себя.

Простой data binding с использованием MXML


Все документы по data binding (которые я прочитал по крайней мере) начинаются именно с данного типа использования этой замечательной функции. К сожалению это практически и единственный метод, который везде наглядно описан. Итак, предположим, что мы имеем два текстовых поля:

<mx:TextInput id="sourceField" text="" />
<mx:TextInput id="destinationField" text="" />


Мы хотим, чтобы изменения в первом поле отображались и на другом поле. Для данной ситуации достаточно написать такой mxml код:

<mx:Binding destination="destinationField.text" source="sourceField.text"/>

Это приведет к тому, что текст, вписанный в первое текстовое поле будет автоматически устанавливаться во второе текстовое поле. В этом и есть смысл простого data binding. Код простого приложения с использованием data binding:

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute">
   <mx:Binding destination="destinationField.text" source="sourceField.text"/>
   <mx:VBox>
      <mx:TextInput id="sourceField" text="" />
      <mx:TextInput id="destinationField" text="" />
   </mx:VBox>
</mx:Application>


Простой data binding с использованием ActionScript


Пример описанный выше легко переписать на ActionScript. Смысл данной конструкции заключается в том, что мы можем использовать её для динамически создаваемых элементов. Итак, всё те же два текстовых поля:

public var sourceField : TextInput = new TextInput();
public var destinationField : TextInput = new TextInput();


Data binding для этих полей будет выглядеть так:

BindingUtils.bindProperty(destinationField, "text", sourceField, "text");

Первым параметром функции bindProperty является объект «пункт назначения», вторым параметром является строка с именем свойства данного объекта, третьим параметром является объект «пункт отправления» и четвертым параметром является объект-цепочка, который при упрощении может являться строкой с именем свойства объекта «пункта отправления» (о данном параметре будет оговорено более подробно ниже). Код приложения с использованием данного метода data binding:

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute"
initialize="init()">
   <mx:Script>
      <![CDATA[
         import mx.controls.TextInput;
         import mx.binding.utils.BindingUtils;
      
         public var sourceField : TextInput = new TextInput();
         public var destinationField : TextInput = new TextInput();
         public function init():void
         {
            sourceField.text = "";
            parentContainer.addChild(sourceField);
            
            destinationField.text = "";
            parentContainer.addChild(destinationField);
            
            BindingUtils.bindProperty(destinationField, "text", sourceField, "text");
         }
               
      ]]>
   </mx:Script>
   <mx:VBox id="parentContainer"/>
</mx:Application>


Объект-цепочка в методе bindProperty класса BindingUtils.


Рассмотрим более подробно объект-цепочку, о котором говорилось ранее. Данный объект используется в качестве 4-го параметра функции bindProperty в классе BindingUtils. Данный объект описывает, каким образом и к какому параметру применить data binding в объекте «пункт отправления». Данный объект может быть представлен в трех различных видах:

Строка

Строка – хранит в себе имя свойства объекта «пункт отправления». То есть, имея текстовое поле с именем «sourceField» мы можем в качестве объекта-цепочки использовать имя опции «text». Об этом виде объекта-цепочки говорилось выше. Дополнительные разъяснения будут лишними.

Массив строк

Массив строк – хранит в иерархию доступа к внутреннему свойству объекта «пункт отправления». Назначение этого вида объекта-цепочки очень легко понять, разобрав простой пример, в котором используются всего лишь два объекта:

package src
{
   public class SimpleObject
   {
      public var myText:String = new String();
      public function SimpleObject():void
      {
         myText = "empty";
      }
   }
}

package src
{
   public class ComplexObject
   {
      public var simpleInstance: SimpleObject = new SimpleObject();
      public function ComplexObject():void
      {
         //some logic
      }
   }
}


Допустим, у нас есть экземпляр класса ComplexObject, с которым нам надо связать текстовое поле, а именно свойство myText (которое находится во внутреннем объекте класса ComplexObject) необходимо связать со свойством text текстового поля. Код инициализации:

public var destinationField : TextInput = new TextInput();
public var complexInstance : ComplexObject = new ComplexObject();


Как было сказано выше, нам необходимо связать complexInstance.simpleInstance.myText с destinationField.text. При помощи data binding это делается так:
BindingUtils.bindProperty(
		destinationField, 
		"text", 
		complexInstance, 
		["simpleInstance","myText"]);


В качестве объекта-цепочки в данном примере используется массив иерархии. В дальнейшем все элементы массива объединяются через точку в одну строку, которая используется в качестве свойства объекта «пункт отправления». Полный код приложения использующего данный метод data binding:

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute"
initialize="init()">
   <mx:Script>
      <![CDATA[
         import src.ComplexObject;
         import mx.controls.TextInput;
         import mx.binding.utils.BindingUtils;
      
         public var destinationField : TextInput = new TextInput();
         public var complexInstance :  ComplexObject = new ComplexObject();
         public function init():void
         {
            destinationField.text = "";
            parentContainer.addChild(destinationField);
            
            BindingUtils.bindProperty(destinationField, 
				"text",
				complexInstance, 
				["simpleInstance","myText"]);
         }
      ]]>
   </mx:Script>
   <mx:VBox id="parentContainer"/>
</mx:Application>


Составной объект

Составной объект – в этом случае объект-цепочка представляет из себя объект типа:

var object : Object = {name:<имя свойства>, getter: <функция получения значения>};

имя свойства — есть строка, в которой содержится имя свойства «объекта отправления»
функция получения значения – данная функция имеет вид:

function (host:<тип объекта «пункта отправления» >) : <тип возвращаемого значения>
{
	//тело функции в которой мы можем обращаться к параметру host,
	// зная что это объект «пункт отправления»
}


Для понимания данного вида объекта-цепочки можно использовать очень простой пример data binding. Например, у нас есть экземпляр класс ArrayCollection и текстовое поле. Условием нашей задачи будет отключение текстового поля при достижении количества объектов содержащихся в нашей коллекции 10 штук. Объявление необходимых переменных:

public var array : ArrayCollection = new ArrayCollection();
public var destinationField : TextInput = new TextInput();


Использование data binding в данном случае:
BindingUtils.bindProperty(
		destinationField, 
		"enabled", 
		array, 
		{
			name:"length",
			getter : function (host : ArrayCollection):Boolean 
						{ return host.length<10; } 
		});


Как мы видим, код прост и понятен. В данном случае data binding будет работать так, как нам необходимо: отключать текстовое поле при количестве элементов в коллекции больше либо равным 10 и соответственно включать при изменении количества элементов в меньшую сторону. Полный код приложения использующего данный метод data binding:

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute"
initialize="init()">
   <mx:Script>
      <![CDATA[
         import mx.collections.ArrayCollection;
         import mx.controls.TextInput;
         import mx.binding.utils.BindingUtils;
      
         public var destinationField : TextInput = new TextInput();
         public var array : ArrayCollection = new ArrayCollection();
         public function init():void
         {
            destinationField.text = "some text";
            parentContainer.addChild(destinationField);
            
            BindingUtils.bindProperty(
				destinationField, 
				"enabled", 
				array, 
				{
					name:"length",
					getter : function (host : ArrayCollection):Boolean 
								{ return host.length>=10; } 
				});
         }
      ]]>
   </mx:Script>
   <mx:VBox id="parentContainer">
      <mx:Button label="add element" click="array.addItem(0);"/>
      <mx:Text text="array length: {array.length}"/>
   </mx:VBox>
</mx:Application>


Метод bindSetter класса BindingUtils.


Рассмотрим другой способ data binding. Данный способ заключается в том, что мы можем вызывать подобие call back функции при изменении свойств объекта «пункта отправления».
Рассмотрим простой пример. Допустим, у нас есть коллекция и текстовое поле. Условия задачи таковы: в коллекции хранятся числовые значения, данные значения добавляются в коллекцию извне. Нам необходимо отображать сумму всех элементов коллекции в текстовом поле (причем сумма должна быть всегда актуальной). Т. е. с точки зрения data binding мы должны, при изменении длинны коллекции, пересчитывать сумму всех её элементов. В данном случае используется метод bindSetter класса BindingUtils.

Для начала инициализация переменных:

public var destinationField : TextInput = new TextInput();
public var array : ArrayCollection = new ArrayCollection();


А теперь опишем data binding операцию:

BindingUtils.bindSetter(calculateSum , array, "length");
Где calculateSum функция вида
function  ( input : <тип свойства объекта «пункт отправления»>):void
{
	//тело функции   
}


В нашем случае реализация функции calculateSum будет такова:

public function calculateSum(input : Number):void
{
	var sum:Number = 0;
	for (var i:int = 0; i<input; i++)
	{
		sum += array[i];
	}
	destinationField.text =  sum.toString();
}


Т. е. все как мы и предполагали. При изменении длинны коллекции, пересчитывается сумма
её элементов и выводится в текстовое поле.
Полный код приложения использующего данный метод data binding:

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute"
initialize="init()">
   <mx:Script>
      <![CDATA[
         import mx.collections.ArrayCollection;
         import mx.controls.TextInput;
         import mx.binding.utils.BindingUtils;
      
         public var destinationField : TextInput = new TextInput();
         public var array : ArrayCollection = new ArrayCollection();
         public function init():void
         {
            destinationField.text = "some text";
            parentContainer.addChild(destinationField);
            
            BindingUtils.bindSetter(calculateSum , array, "length");
         }
         
         public function calculateSum(input : Number):void
         {
            var sum:Number = 0;
            for (var i:int = 0; i<input; i++)
            {
               sum += array[i];
            }
            destinationField.text =  sum.toString();
         }

      ]]>
   </mx:Script>
   <mx:VBox id="parentContainer">
      <mx:Button label="add element" click="array.addItem(Math.random());"/>
      <mx:List width="100" height="200" dataProvider="{array}"/>
   </mx:VBox>
</mx:Application>


Использование метода bindSetter с составным видом объекта-цепочки


Это самый сложный вид data binding, но это не значит, что он очень сложен для понимания. Традиционно вернемся к постановке задачи.
Необходимо написать класс-коллекционер который собирает данные о изменениях произошедших с наблюдаемым компонентом. Класс будет хранить массив из информационных объектов (с шириной, высотой и позицией наблюдаемого компонента).
Напишем для начала класс:

package src
{
   import mx.collections.ArrayCollection;  
   public class Collector
   {
      public var array : ArrayCollection;      
      public function Collector():void
      {
         array = new ArrayCollection();
      }      
      public function collect(str:String):void
      {
         array.addItem(str);
      }
   }
}


Приложение будет выглядеть так:

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute"
initialize="init()">
   <mx:Script>
      <![CDATA[
         import mx.core.UIComponent;
         import mx.binding.utils.BindingUtils;
         import src.Collector;
         public var collector : Collector = new Collector();
         
         public function init():void
         {
            BindingUtils.bindSetter(collector.collect, button, {name:"width", getter: returnInfo});
            data.dataProvider = collector.array;
         }
         
         public function returnInfo(host:UIComponent):String
         {
            return "width:" + host.width + "; height:" + host.height;
         }
         
         public function changeButton():void
         {
            button.width = Math.round(Math.random() * 200);
            button.height = Math.round(Math.random() * 100);   
         }
      ]]>
   </mx:Script>
   <mx:VBox>
   <mx:Button label="change" click="changeButton();"/>
   <mx:List id="data" width="200" height="400" />
   <mx:Button id="button" width="200" height="100" label="mybutton"/>
   </mx:VBox>
</mx:Application>


Обращаем внимание на функцию init() а именно строку

BindingUtils.bindSetter(collector.collect, button, {name:"width", getter: returnInfo});

Она несет в себе информацию о том, что при изменении свойства «width» компонента button у нас будет вызываться функция returnInfo, которая сформирует данные о кнопке, эти данные будут автоматически переданы в функцию collector.collect() в которой они будут соответствующим образом обработаны.

Вот собственно и всё, что я хотел рассказать.

P.S. Топик специально писался для блога Adobe Flex.
Я думаю, он туда обязательно попадёт.
+25
30 октября 2008, 15:44
44
oss

комментарии (19)

0
Doggy #
Отлично — Первый стоящий топик по Flex =)

Надеюсь у меня тоже найдётся как-нибудь времячко поделиться накопленным))

НЛО прилетело и опубликовало эту надпись здесь
0
quiet_drago #
как-раз в последнее время их становится всё больше, собственно, флекс входит в зенит)

замечательная тенденция. тоже буквально несколько месяцев как взялся за него.
может со временем тож на статью наскребу)

автору — Спасибо.
статья найдет применение в моих проектах)
+1
romank #
благодарю. Как раз в текущем проекте flex используется. Думаю, что ваша статья пригодится. В избранное!
0
oss #
Большое спасибо за отзывы!
0
romank #
Пишите еще, будет интересно почитать
0
avenu #
Текст не очень понятный, но с примерами вроде ничего.
+1
ashoot #
Огромное спасибо, за доходчивый и интересный матерьял. Если бы большинство статей на хабре были такого содержания, был бы второй Experts-Exchange.
0
DaFive #
Благодарствую. Как флешеру пригодится.
0
fzn7 #
Спасибо за интересный топик. Я в своих проектах не использую так широко байндинг, а в особенности цепочки. Мне проще переопределить сеттеры для изменения состояния компонентов. Теперь пересмотрю реализацию некоторых вещей.
+2
GreyWind #
А почему так сложно? Есть же прямой и куда более внятный способ биндования:

>>Условием нашей задачи будет отключение текстового поля при достижении количества объектов содержащихся в >>нашей коллекции 10 штук

<?xml version=«1.0» encoding=«utf-8»?>
<mx:Application
xmlns:mx=«www.adobe.com/2006/mxml»
layout=«horizontal»>
<mx:ArrayCollection id=«data_collection» />
<mx:Button click=«data_collection.addItem('item')» label=«Add item to collection» />
<mx:Label text=«Items in collection: {data_collection.length}» />
<mx:TextInput enabled="{data_collection.length < 10}" />
</mx:Application>

Мы используем прямой биндинг через фигурные скобки в XML для значения поля enabled. Вот и все. Единственное неудобство, нужно использоавть HTML entity вместо простого знака «меньше».

Повторюсь, самый простой и понятный способ биндования — это именно через фигурные скобки в MXML. Биндинги через BindingUtils нужны если вам надо связать значение в динамически созданном объекте с каким-то другим значением в вашем классе. Например привязать всплывающее окно редактирования, к выбранному в списке объекту.
0
watabou #
И самый распространенный по-моему…
0
oss #
Данный пример был приведен для того, чтобы разъяснить, что такое составной объект в методе bindProperty и не более того. Никто не говорит, что надо делать именно так. Я просто пытался на простых примерах объяснить механизм.

Да, вы правы на счет простого и понятного способа через фигурные скобки в mxml, но бывают ситуации, когда этого недостаточно. Я имею в виду такие задачи, когда идет работа с динамически создаваемыми компонентами.

Например, у меня была задача, когда необходимо было, чтобы дочерние элементы, лежащие на компоненте, имели соотносимые с родительскими координаты и размеры. Т.е. в случае изменения размера родительского компонента необходимо пересчитывать координаты и размеры каждого из дочерних элементов. Плюс ко всему этому дочерние элементы создавались динамически. Эта задача очень легко решилась при помощи связывания, а именно использования метода bindSetter совместно со составным объектом.

Я думаю, что каждый найдет применение какого-то из вышеописанных методов в своей уникальной задаче.
+2
yateam #
а где собственно tricks? все что вы написали есть в официальной документации и туториалах
0
burzilov #
«Бэн! Это Данила! Ай нид хэлп...» (с)

Пытаюсь использовать цепочку для простенького примера: есть массив, состоящий из элементов типа Object, и есть Label, который должен отображать конкретный атрибут конкретного элемента этого массива. Пробовал вот так:
BindingUtils.bindProperty(myLabel, «text», this, [«myArray[1]», «myAttr»]);

Не работает.

0
oss #
С точки зрения массива (имеется в виду Array) мы можем использовать только одно свойство, на которое можно повесить binding это length. Он будет срабатывать только тогда, когда у массива изменяется количество элементов.
Для твоего случая лучше использовать bindProperty:

BindingUtils.bindProperty(myLabel, «text», myArray,
{
name: «length»,
getter: function (host: ArrayCollection):String { return host[1].myAttr; }
});

Примерно так. Все зависит от задачи. Если данный подход это все, что тебе нужно тогда отлично, иначе придется обдумывать как избавиться от массива. Обращайся, обдумаем.
0
burzilov #
Проблема решилась, спасибо за ответ.
Прокатило следующее:
BindingUtils.bindProperty(myLabel, «text», this, [«myArray», «1», «myAttr»]);
т.е. в цепочке участвует и номер элемента.
0
vladislavkorobov #
Большое спасибо автору.
В документации искать долго и не так понятно.
+1
amid_ukr #
BindingUtils — dynamic программинг, в enterprise стоит юзать в очень крайнем случае. MXMXL умеет хорошо делать тоже с плюс type check-инг, да и выглядит оно поприятнее.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.