Мартин Фаулер просвещает
На конференции после моего доклада о Cairngorm
(да и после конференции тоже) некоторые у меня спрашивали, а можно ли реализовать
data binding в чистых AS проектах, без Flex.
И я сразу предложил идею, что первой пришла в голову:
в ModelLocator вместо public свойств использовать геттеры-сеттеры, из сеттеров
рассылать события об изменении значения, в видах подписываться на эти события, и
в обработчиках устанавливать значения контролов. Конечно, это будет работать,
но мой ответ никого не обрадовал. Сразу очевидно, какое море кода нужно написать.
А стоит ли оно того? Пожалуй не стоит.
А потом я почитал статью Мартина Фаулера
GUI Architectures,
которая у меня давно была намечена к прочтению. В основном благодаря тому,
что один добрый человек --
Илья Черкасов aka Acerv,
взял на себя труд перевести ее на русский язык
(а читать на русском я как-то больше люблю :)
Он выложил свой перевод на на habrahabr.ru в виде цикла статей:
В статье рассказывается про способы работы с GUI, про Model-View-Controller
и Model-View-Presenter, про слои представления данных, и про другие любопытные вещи.
Признаться, я понял далеко не все. Зато понял, как сделать data binding,
не сложный в реализации и использовании.
Друзья мои! Все давным давно придумано до нас, еще в 80-м году разработчиками Smalltalk.
Именно они начали задумываться об оптимальных способах работы с GUI,
придумали MVC, data binding и другие полезные вещи.
Стоило только прочитать в 3-й части про объект-обертку и про AspectAdaptor, как
я сразу понял, как это реализовать на AS.
Объект-обертка
Итак, объект-обертка -- это специальный класс, который хранит внутри себя
одно-единственное значение, предоставляет к нему доступ через пару геттер-сеттер,
и в сеттере генерирует событие, если хранимое значение изменилось.
Реализуется все это очень просто, смотрите код. К описанному функционалу я
еще добавил контроль за типом данных.
package util
{
import flash.events.EventDispatcher;
public class ValueHolder extends EventDispatcher
{
// properties
private var value: *;
private var type: Class;
// constructor
public function ValueHolder(value:*, type:Class)
{
this.value = value;
this.type = type;
}
// getters & setters
public function get Value():* { return this.value; }
public function set Value(value:*):void
{
if(!(value is this.type)) throw('Invalid data type');
if(this.value == value) return;
var oldValue:* = this.value;
this.value = value;
this.dispatchEvent(new ChangeEvent(oldValue, this.value));
}
}
}
Все настолько просто и очевидно, что и комментировать особо нечего.
Нужно только еще показать код ChangeEvent, хоть там и нет ничего примечательного.
package util
{
import flash.events.Event;
public class ChangeEvent extends Event
{
// constants
static public const CHANGE: String = 'ChangeEvent';
// properties
public var oldValue:*;
public var newValue:*;
// constructor
public function ChangeEvent(oldValue:*, newValue:*)
{
this.oldValue = oldValue;
this.newValue = newValue;
super(CHANGE);
}
}
}
Использование объектов-оберток в модели
И что дальше? А дальше мы храним в модели не просто значения, а значения,
обернутые в такие классы. Примерно так:
package model
{
import util.ValueHolder;
public class SampleModel
{
// properties
public var totalBananas: ValueHolder = new ValueHolder(10, int);
public var bananaColor: ValueHolder = new ValueHolder(0xffff00, int);
// constructor
public function SampleModel()
{
}
}
}
Уже неплохо. Можно получать значения из модели, присваивать новые значения:
var total:int = model.totalBananas.Value; model.totalBananas.Value = 25;
Можно подписываться на события и ловить изменения значения. Это еще не data binding,
но уже польза, ибо это лучше, чем иметь кучу сеттеров в модели, генерирующих события.
По сути мы эти сеттеры перенесли в ValueHolder.
Подключаем привязку данных
Теперь вынесем в отдельный класс подписку на событие и обработчик оного:
package util
{
public class Binder
{
// properties
private var dest: Object;
private var prop: String;
// constructor
public function Binder(source:ValueHolder, dest:Object, prop:String)
{
this.dest = dest;
this.prop = prop;
this.dest[this.prop] = source.Value;
source.addEventListener(ChangeEvent.CHANGE, OnPropChange);
}
// methods
private function OnPropChange(ev:ChangeEvent):void
{
this.dest[this.prop] = ev.newValue;
}
}
}
Здесь тоже все очевидно. Теперь нам не нужно явно подписываться на кучу событий
и определять кучу обработчиков. Нужно только создать биндеры, а это не так уж
сильно отличается от того, что есть во Flex.
var binder:Binder = new Binder(this.sampleModel.totalBananas, tf, 'text');
Пробуем на практике
И небольшое приложение, где все это используется. Здесь есть текстовое поле,
к которому мы привязываем значение из модели, и таймер, который будет
обновлять модель через промежутки времени.
package
{
import flash.display.Sprite;
import flash.events.TimerEvent;
import flash.text.TextField;
import flash.utils.Timer;
import model.SampleModel;
import util.*;
public class TryDataBinding extends Sprite
{
// properties
private var sampleModel: SampleModel;
// constructor
public function TryDataBinding()
{
var tf:TextField = new TextField();
tf.width = 100;
tf.height = 30;
tf.border = true;
this.addChild(tf);
this.sampleModel = new SampleModel();
var binder:Binder = new Binder(this.sampleModel.totalBananas, tf, 'text');
var timer:Timer = new Timer(1000);
timer.addEventListener(TimerEvent.TIMER, OnTimer);
timer.start();
}
// methods
private function OnTimer(ev:TimerEvent):void
{
this.sampleModel.totalBananas.Value += 10;
}
}
}
Ну вот, привязка данных в действии.
Что дальше
Это довольно простой код, но весьма полезный и вполне годный к использованию в
проектах. Но есть два нюанса, которые требуют доработки.
Во-первых, ValueHolder неплохо справляется с примитивными типами данных,
а со сложными все будет чутка сложнее :)
if(this.value == value) return;
Тут у нас есть сравнение нового значения со старым. Сложные объекты так просто не
сравнишь, нужно определять функцию, которая умеет это делать, и как-то использовать
ее здесь. Но это все решаемо:
можно добавить в класс третий параметр -- compareFunction, и передать в конструктор
третий (не обязательный) аргумент.
Во-вторых, я не проверял, что будет с расходом и освобождением памяти, но уверен,
что будет не так хорошо, как хочется. Если в вашем проекте все виды создаются на
старте приложения и живут все время работы приложения, то особых проблем нет.
А если виды создаются и удаляются динамически, то придется
вручную позаботиться об освобождении памяти: при удалении вида все биндеры нужно
будет отписать от событий и уничтожить. Значит, все биндеры нужно хранить.
Грамотно освобождать память -- задача довольно трудоемкая. И раз уж приходится этим
заниматься, то помимо всего прочего нужно позаботиться еще и о биндерах. Само по
себе использование биндеров не усложняет и не упрощает эту задачу.
Не было бы привязки данных, события все равно были бы, и от них нужно
было бы отписываться. Слабые ссылки?
А их, наверное, и в классе биндера можно использовать.

супер!
спасибо! очень полезная информация! особенно Ваши комментарии в конце
считаю Ваш fлог самым лучшим по теме as / flex и связанным технологиям
слава и почет Вам!
Пасиб на добром слове. Статья
Пасиб на добром слове.
Статья старова-та. Так что более полезной информацией будет то, что можно смело юзать [Bindable] в чистых AS3 проектах. Это не потянет зависимости от всего флекс фреймворка, а добавит всего-лишь 4Kb веса к вашему swf. Проверено.