Апр 08 2011

Биндинг в ActionScript проектах. Часть 3

Category: ActionScript 3.0Diestro @ 8:52

В предыдущих статьях мы рассмотрели возможности биндинга в плане привязки метатега [Bindable] к различным элементам нашего кода и кастомизации срабатывания биндинга с помощью событий. На этом возможности биндинга не ограничиваются. Подписка на события биндинга в методах BindingUtlis.bindProperty и BindingUtlis.bindSetter также дает большое количество возможностей для его реализации.

Для того чтобы слушать изменения свойства или метода, помеченного как [Bindable], необходимо использовать один из методов BindingUtlis.bindProperty или BindingUtlis.bindSetter.

public static function bindProperty (site:Object, prop:String, host:Object, chain:Object, commitOnly:Boolean = false, useWeakReference:Boolean = false):ChangeWatcher
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: first keyword
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 за помощь в обсуждении и написании этих постов.

Tags: ,

3 комментария »

  1. комментарий by fmaxx — 17 мая 2011 @ 16:56

    спасибо большое, достаточно непростая тема байндинга.

  2. комментарий by firsoff_max — 30 мая 2011 @ 8:42

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

  3. комментарий by marat_sa — 19 июня 2011 @ 16:14

    Спасибо за статьи о биндинге =)
    Очень пригодились.

RSS лента комментариев к этой записи. TrackBack URL

Оставить комментарий

 

Comment Spam Protection by WP-SpamFree