Апр 08 2011
Биндинг в ActionScript проектах. Часть 3
В предыдущих статьях мы рассмотрели возможности биндинга в плане привязки метатега [Bindable] к различным элементам нашего кода и кастомизации срабатывания биндинга с помощью событий. На этом возможности биндинга не ограничиваются. Подписка на события биндинга в методах BindingUtlis.bindProperty и BindingUtlis.bindSetter также дает большое количество возможностей для его реализации.
Для того чтобы слушать изменения свойства или метода, помеченного как [Bindable], необходимо использовать один из методов BindingUtlis.bindProperty или BindingUtlis.bindSetter.
public static function bindSetter (setter:Function, host:Object, chain:Object, commitOnly:Boolean = false, useWeakReference:Boolean = false):ChangeWatcher
Единственное принципиальное отличие этих двух методов заключается в том что в первом случае в результате срабатывания биндинга меняется значение указанного свойства prop в объекте site, во втором вызывается метод setter для обработки результата. В остальном эти два метода принимают одинаковые параметры и дальнейшее повествование будет иметь одинаковый смысл для обоих случаев.
Параметр host
Параметр host это объект в котором находится параметр или цепочка параметров за которыми необходимо следить.
Параметр chain
Итак, и метод bindProperty и bindSetter имеют обязательный параметр сhain типа Object, который может принимать следующие значения:
chain в виде строки
Определяет имя привязываемого свойства:
1 | BindingUtils.bindSetter(onPropChange, host, "prop"); |
До сих пор мы в качестве chain всегда передавали именно строку и об использовании данного параметра в таком качестве можно прочесть в предыдущих статьях;
chain в виде массива
Непустой массив, содержащий последовательность свойств за которыми необходимо следить:
1 | BindingUtils.bindProperty(this, "prop", host, ["a", "b", "c"]); |
Массив в качестве цепочки крайне удобен и используется достаточно часто. Цепочка свойств должна быть указана в виде ["a","b","c"] что будет соответствовать привязке свойства host.a.b.c. Может возникнуть вполне резонный вопрос: “Почему в качестве host не указать сразу host.a.b, а свойство за которым следить не передать в качестве строки "c"?
1 | BindingUtils.bindProperty(this, "prop", host.a.b, "c"); |
Цепочкой целесообразно пользоваться когда в ней встречаются изменяющиеся свойства тоже помеченные метатегом [Bindable]. Утилиты биндинга автоматически будут распознавать изменение любого из таких свойств в цепочке и нам не понадобится городить биндинг в биндинге, следить чтобы экземпляры ChangeWatcher были отписаны и т.п.
Представим что у нас есть класс Model в котором есть свойство item типа Item:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | package{ import flash.events.EventDispatcher; public class Model extends EventDispatcher{ [Bindable] public var item:Item; public function Model() { super(); } } } |
Класс Item в свою очередь содержит свойство color
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | package{ import flash.events.EventDispatcher; public class Item extends EventDispatcher{ [Bindable] public var color:uint; public function Item(){ super(); } } } |
Так же есть класс отображения CurrentColorView который должен показывать текущий цвет для текущего элемнта item в классе Model.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | package{ import flash.display.Sprite; import mx.binding.utils.BindingUtils; public class CurrentColorView extends Sprite{ private var model:Model; public function CurrentColorView(model:Model){ this.model = model; super(); BindingUtils.bindSetter(onColorChange, model, ["item", "color"]); } private function onColorChange(value:int):void{ // меняем отображение цвета graphics.clear(); graphics.beginFill(value); graphics.drawCircle(0,0,50); graphics.endFill(); } } } |
В данном примере свойство за которым нужно следить задается как раз в виде цепочки. При смене экземпляра в свойстве item класса Model биндинг сработает автоматически и метод onColorChange получит значение цвета уже для нового элемента. То же самое произойдет и при изменении свойства color в экземпляре класса, содержащемся в item класса Model.
Если бы не было возможности задавать отслеживаемые свойства в виде цепочек код пришлось бы усложнить, например написать следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | // Внимание! Говнокод! package{ import flash.display.Sprite; import mx.binding.utils.BindingUtils; import mx.binding.utils.ChangeWatcher; public class CurrentColorView extends Sprite{ private var model:Model; // хранилище для "наблюдателя" за свойтсвом color private var colorChangeWatcher:ChangeWatcher; public function CurrentColorView(model:Model){ this.model = model; super(); // подписываемся на изменения свойства item в model BindingUtils.bindSetter(onItemChange, model, "item"); } // обработчик вызываемый при изменении свойства item в классе Model private function onItemChange(value:Item):void{ // если мы уже наблюбаем за свойством color отписываемся от наблюдения if (colorChangeWatcher) colorChangeWatcher.unwatch(); // подписываемся на изменения свойства color в новом экземпляре свойства item класса Model colorChangeWatcher = BindingUtils.bindSetter(onColorChange, value, "color"); } // Цвет изменился private function onColorChange(value:int):void{ // меняем отображение цвета graphics.clear(); graphics.beginFill(value); graphics.drawCircle(0,0,50); graphics.endFill(); } } } |
При увеличении уровня вложенности код будет усложняеться еще сильней, поэтому использование цепочек в качестве параметра для chain очень сильно упрощает жизнь и делает код более лаконичным и понятным.
chain в виде объекта {name: имя свойства, getter: имя метода}
В случае когда для параметра chain мы передаем объект в виде {name: имя свойства, getter: имя метода} мы можем подписаться на прослушивание изменения свойства name указаного в виде строки для объекта host. В результате срабатывания биндинга будет вызван метод указанный в свойстве getter который в качестве параметра получит объект host. Этот метод должен возвращать значение, совпадающее по типу со свойством указанным в качестве приемника в случае bindProperty, либо совпадающее по типу с параметром, поступающим в метод обработчика в случае с bindSetter. Т.е. мы можем передать нашему слушателю (свойству или методу) не то значение которое получено, а новое вычисленное значение или объект другого типа после нужного нам преобразования.
Приведу пару примеров:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | package { import flash.display.Sprite; import mx.binding.utils.BindingUtils; public class Viewer extends Sprite { private var viewObject:Sprite = new Sprite(); [Bindable] public var keyWord:String = ""; public function Viewer() { BindingUtils.bindProperty(viewObject, "visible", this, {name:"keyWord", getter: function(host:Object):Boolean{return host.keyWord == "showIt!"}}); } } } |
В данном примере у нас есть спрайт viewObject, параметр visible которого будет выставлен в true только в случае когда свойство keyWord примет значение "showIt!". Т.е. при изменении свойства keyWord вызовется анонимный метод, указанный в поле getter:, возвращающий булево значение. Это значение будет true только в случае совпадения со строкой "showIt!". Возвращенное значение будет присвоено параметру visible для viewObject.
Рассмотрим пример в случае использования bindSetter:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | package { import flash.display.Sprite; import mx.binding.utils.BindingUtils; public class KeyWordLogger extends Sprite { public var viewObject:Sprite = new Sprite(); [Bindable] public var keyWord:String = "first keyword"; public function KeyWordLogger() { BindingUtils.bindSetter(log, this, {name:"keyWord", getter: function(host:Object):String{return "New keyword is: " + host.keyWord}}); keyWord = "second keyword"; keyWord = "wrong keyword"; } private function log(value:String):void { trace(value); } } } |
В результате выполнения данного кода в консоли мы увидим следующее:
New keyword is: second keyword
New keyword is: wrong keyword
Суть такая же как в и в предыдущем примере только результат возвращенный анонимной функцией будет присвоен не свойству, а передан в качестве параметра методу log, который выводит в консоль получееную строку.
На самом деле приведенной выше конструкцией chain в виде объекта {name: имя свойства, getter: имя метода} я никогда не пользовался, и честно говоря не могу придумать задач для которых это бы было единственным и правильным решением. Я не сторонник написания анонимных функций и считаю что их присутствие в коде влияет на его читабельность и прозрачность не в лучшую сторону, а всю логику, которую можно описать задавая анонимную функцию в качестве getter, можно вынести в метод обработки для bindSetter и сделать все там. Как показал беглый поиск по Flex фреймворку в SDK 3-их и 4-х версий его разработчики тоже не используют данный тип конструкции.
Если кто-то готов показать примеры когда использование анонимного getter дает какие-то преимущества по сравнению с другими способами биндинга готов выслушать и дополнить ими статью.
Параметр commitOnly
Использование данного параметра позволяет разделить изменения свойства за которым нужно следить на “текущее изменение” и “окончательное (фиксирующее) изменение”. Например пользователь в какой то форме с несколькими текстовыми полями делает изменения. Нам необходимо следить за изменениями текстовых полей и нужно знать окончательное это изменение в текстовом поле или пользователь продолжает туда вводить информацию, в зависимости от этого осуществлять валидацию введённой информации.
Если данный параметр выставлен в true, то это означает что мы подписываемся на прослушивания только окончательных изменений в наблюдаемом свойстве. Если же в качестве параметра будет передано false то слушать будем все изменения.
Для указания события для текущих (нефиксирующих) изменений существует метатег [NonCommittingChangeEvent(event="<имя события>")]. Выставлять commitOnly в true имеет смысл только когда перед наблюдаемым свойством присутствует этот метатег. Если commitOnly выставлен в true, то для событий указанных в данном метатеге биндинг срабатывать не будет. Таким образом указывая метатеги [NonCommittingChangeEvent(event="<имя события>")] и [Bindable(event="<имя события>")] в различных сочетаниях перед наблюдаемым свойством мы можем разделять срабатывание биндинга для фиксирующих и текущих изменений.
Приведу пример исользования данного флага. Допустим нам необходим компонент TextInputField со свойством inputText к которому можно привязать биндинг. Биндинг должен срабатывать при любом изменении текста в компоненте, фиксирующий биндинг должен сработать при потере компонентом фокуса.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | package { import flash.text.TextField; import flash.text.TextFieldType; public class TextInputField extends TextField { public function TextInputField() { super(); type = TextFieldType.INPUT; border = true; } // Измение текста в текстовом поле [NonCommittingChangeEvent(event="change")] // Потеря фокуса тестовым полем [Bindable(event="focusOut")] public function get inputText():String { return super.text; } } } |
Здесь для геттера inputText указано два события для срабатывания биндинга: изменение текста и потеря фокуса. В данном случае нам даже не придется генерировать эти события, их генерирует сам класс TextField от которого мы отнаследовались.
Теперь рассмотрим пример использования класса TextInputField
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | package { import flash.display.Sprite; import mx.binding.utils.BindingUtils; public class Form extends Sprite { private var inputField:TextInputField = new TextInputField(); public function Form () { inputField.width = 200; inputField.multiline = true; inputField.wordWrap = true; addChild(inputField); // биндимся на все изменения свойства inputText BindingUtils.bindSetter(onInputChange, inputField, "inputText"); // биндимся только на фиксацирующие изменения свойства inputText BindingUtils.bindSetter(onInputCommit, inputField, "inputText", true); } private function onInputChange(value:String):void { trace("Текущее изменение: " + value); } private function onInputCommit(value:String):void { trace("Фиксирующее изменение: " + value); } } } |
В данном примере мы биндимся два раза на свойство inputText, первый раз для слежения за всеми изменениями этого свойства, второй раз для отслеживания только фиксирующих изменений. В первом случае для обработки будет вызван метод onInputChange, во втором onInputCommit.
Параметр useWeakReference
Данный параметр имеет тот же смысл что и в случае подписки на события посредством addEventListener. Он определяет является ли ссылка сильной (false) или слабой (true). По умолчанию ссылка является сильной. В случае слабой ссылки использование биндинга не будет влиять на удаление сборщиком мусора объекта указнного в host из памяти.
На этом позвольте откланяться, это все что я хотел рассказать о биндинге в ActionScript проектах.
p.s. Огромное спасибо друзьям и коллегам из родного Drimmi за помощь в обсуждении и написании этих постов.

спасибо большое, достаточно непростая тема байндинга.
с удивлением обнаружил при релизе проекта – байндинг отвалился, flex SDK 4.0 ( при откате назад на debug версию – байндинг работает), попробовал под FlexSDK 4.5 – полет нормальный!
еще раз спасибо!
Спасибо за статьи о биндинге =)
Очень пригодились.