На конференции после моего доклада о 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
{
private var value: *;
private var type: Class;
public function ValueHolder(value:*, type:Class)
{
this.value = value;
this.type = type;
}
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
{
static public const CHANGE: String = 'ChangeEvent';
public var oldValue:*;
public var newValue:*;
public function ChangeEvent(oldValue:*, newValue:*)
{
this.oldValue = oldValue;
this.newValue = newValue;
super(CHANGE);
}
}
}
И что дальше? А дальше мы храним в модели не просто значения, а значения, обернутые в такие классы. Примерно так:
package model
{
import util.ValueHolder;
public class SampleModel
{
public var totalBananas: ValueHolder = new ValueHolder(10, int);
public var bananaColor: ValueHolder = new ValueHolder(0xffff00, int);
public function SampleModel()
{
}
}
}
Уже неплохо. Можно получать значения из модели, присваивать новые значения:
var total:int = model.totalBananas.Value;
model.totalBananas.Value = 25;
Можно подписываться на события и ловить изменения значения. Это еще не data binding, но уже польза, ибо это лучше, чем иметь кучу сеттеров в модели, генерирующих события. По сути мы эти сеттеры перенесли в ValueHolder.
Теперь вынесем в отдельный класс подписку на событие и обработчик оного:
package util
{
public class Binder
{
private var dest: Object;
private var prop: String;
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);
}
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
{
private var sampleModel: SampleModel;
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();
}
private function OnTimer(ev:TimerEvent):void
{
this.sampleModel.totalBananas.Value += 10;
}
}
}
Ну вот, привязка данных в действии.
Это довольно простой код, но весьма полезный и вполне годный к использованию в проектах. Но есть два нюанса, которые требуют доработки.
Во-первых, ValueHolder неплохо справляется с примитивными типами данных, а со сложными все будет чутка сложнее :)
if(this.value == value) return;
Тут у нас есть сравнение нового значения со старым. Сложные объекты так просто не сравнишь, нужно определять функцию, которая умеет это делать, и как-то использовать ее здесь. Но это все решаемо: можно добавить в класс третий параметр -- compareFunction, и передать в конструктор третий (не обязательный) аргумент.
Во-вторых, я не проверял, что будет с расходом и освобождением памяти, но уверен, что будет не так хорошо, как хочется. Если в вашем проекте все виды создаются на старте приложения и живут все время работы приложения, то особых проблем нет. А если виды создаются и удаляются динамически, то придется вручную позаботиться об освобождении памяти: при удалении вида все биндеры нужно будет отписать от событий и уничтожить. Значит, все биндеры нужно хранить.
Грамотно освобождать память -- задача довольно трудоемкая. И раз уж приходится этим заниматься, то помимо всего прочего нужно позаботиться еще и о биндерах. Само по себе использование биндеров не усложняет и не упрощает эту задачу. Не было бы привязки данных, события все равно были бы, и от них нужно было бы отписываться. Слабые ссылки? А их, наверное, и в классе биндера можно использовать.
Add new comment