Skip to main content

Data Binding на чистом AS, без Flex

Мартин Фаулер просвещает

На конференции после моего доклада о 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, и передать в конструктор
третий (не обязательный) аргумент.

Во-вторых, я не проверял, что будет с расходом и освобождением памяти, но уверен,
что будет не так хорошо, как хочется. Если в вашем проекте все виды создаются на
старте приложения и живут все время работы приложения, то особых проблем нет.
А если виды создаются и удаляются динамически, то придется
вручную позаботиться об освобождении памяти: при удалении вида все биндеры нужно
будет отписать от событий и уничтожить. Значит, все биндеры нужно хранить.

Грамотно освобождать память -- задача довольно трудоемкая. И раз уж приходится этим
заниматься, то помимо всего прочего нужно позаботиться еще и о биндерах. Само по
себе использование биндеров не усложняет и не упрощает эту задачу.
Не было бы привязки данных, события все равно были бы, и от них нужно
было бы отписываться. Слабые ссылки?
А их, наверное, и в классе биндера можно использовать.

No votes yet

супер!

спасибо! очень полезная информация! особенно Ваши комментарии в конце
считаю Ваш fлог самым лучшим по теме as / flex и связанным технологиям
слава и почет Вам!

Пасиб на добром слове. Статья

Пасиб на добром слове.

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