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

Tags:

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

На конференции после моего доклада о 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

Filtered HTML

  • Web page addresses and e-mail addresses turn into links automatically.
  • Allowed HTML tags: <a> <em> <strong> <cite> <blockquote> <code> <ul> <ol> <li> <dl> <dt> <dd>
  • Lines and paragraphs break automatically.

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.
CAPTCHA
question for bots )
Image CAPTCHA
Enter the characters shown in the image.