Как-то блуждал по блогам и наткнулся на страничку с описанием интересной скрытой возможности XML в ActionScript 3 – метод XML.setNotification(). Этот метод позволяет получать данные об изменениях в XML дереве, за которым установили такое наблюдение. Я воспользовался этим методом и написал ряд классов, которые позволяют получать события об изменениях в XML дереве и использовать биндинг с XML во Flex.
Список используемых классов:
- aw.xml.XMLNotifier – Класс используемый для привязки EventDispatcher'а к XML узлу. Вещает событие XMLEvent.CHANGED при каждом изменении XML дерева, к которому привязан.
- aw.xml.cover.XMLCoverProxy – Базовый класс позволяющий пользоваться синтаксисом E4X и обращаться к внутреннему XML/XMLList объекту без препятствий.
- aw.xml.cover.XMLCoverList – Используется для прослушивания изменений в узлахсодержащихся в XMLList и обращения к его свойствам и методам.
- aw.xml.cover.XMLCover – Используется для прослушивания узла XML и обращения к его свойствам и методам.
- aw.events.XMLEvent – Событие изменения XML дерева. Содержит константы доступных событий.
XMLCover, основной класс, используемый для управления XML деревом. Но, если вам необходимо просто прослушивать изменения в XML дереве, то можете просто пользоваться классом XMLNotifier. Экземпляры этого класса, создаются только через статический метод getInstance().
Пример использования XMLNotifier:
import aw.events.XMLEvent; import aw.xml.XMLNotifier; var xml:XML = <root></root>; var notifier:XMLNotifier = XMLNotifier.getInstance(xml); notifier.addEventListener(XMLEvent.CHANGED, this.changedHandler); function changedHandler(e:XMLEvent):void{ trace(e.changeType); } xml.@attr = true; // attributeAdded xml.appendChild(<child/>); // nodeAdded xml.child = 'text value'; // textSet xml.child.setNamespace('urn:some-uri'); // namespaceSet
Весь пакет можно использовать вместе с Flex SDK и отдельно, к примеру, во Flash CS3. Но только при использовании совместно с Flex SDK для вас будет доступен биндинг. Первое название его было XMLWatcher, но пришлось переименовать из-за конфликта имён с mx.binding.XMLWatcher.
Классы XMLCover и XMLCoverList хранят ссылку на XML/XMLList объекты, но не мешают доступу к их свойствам, узлам и методам, т.е. создав экземпляр XMLCover, вы сможете через него делать практически любые операции с XML деревом. Практически любые, потому что я не смог реализовать фильтрацию списков через синтаксис E4X, к примеру
xml.child.(@id=="5")[0].setName("newName");
выражение в круглых скобках и есть фильтр.
Оба класса содержат собственные объекты EventDispatcher и оба могут испускать события, сообщая о происходящем. Но сами они не слушают XML дерево, они пользуются услугами класса XMLNotifier, пересылая его события об изменении дерева.
Класс XMLNotifier использует кэширование экземпляров класса. Каждый созданный экземпляр класса привязывается к своему узлу и при следующем обращении к этому узлу, проверяется существование уже привязанного к нему объекта и если таковой находится, то его вы и получите.
Конструктор класса XMLCover принимает такие, не обязательные аргументы:
- узел XML об изменениях состояния, которого нужно оповещать.
- булево значение, указывающее на создание дочерних объектов XMLCover/XMLCoverList через синтаксис E4X. Любая попытка получить дочерний узел или список узлов будет приводить к созданию соответствующего объекта XMLCover или XMLCoverList, соответственно. Это необходимо для реализации биндинга во Flex и это простой способ подписаться на события дочерних узлов. По умолчанию, "true".
import aw.xml.XMLCover; import aw.utils.object_utils; import flash.utils.getQualifiedClassName; var xml:XML = <root><child/></root>; var cover:XMLCover = new XMLCover(xml); // createTree = true, по умолчанию trace(getQualifiedClassName(cover.child)); // aw.xml.cover::XMLCoverList trace(getQualifiedClassName(cover.child[0])); // aw.xml::XMLCover cover.object_utils::createTree = false; trace(getQualifiedClassName(cover.child)); // XMLList trace(getQualifiedClassName(cover.child[0])); // XML
Конструктор XMLCoverList принимает объект XMLList в качестве первого аргумента, а второй аргумент совпадает со вторым аргументом конструктора класса XMLCover.
Оба класса содержат методы, описанные IEventDispatcer и несколько контрольных свойств в пространстве имён aw.utils.object_utils.
Свойства класса XMLCover, в пространстве имён aw.utils.object_utils:
- xml – оригинальный объект XML. Свойство только для чтения.
- notifier – возвращает ссылку на объект XMLNotifier используемый с данным объектом. Свойство только для чтения.
- createTree – указывает на создание дочерних объектов XMLCover/XMLCoverList через синтаксис E4X.
Свойства класса XMLCoverList, в пространстве имён aw.utils.object_utils:
import aw.xml.XMLCover; import aw.utils.object_utils; import aw.xml.cover.XMLCoverList; var xml:XML = <root><child/><child/><child/></root>; var cover:XMLCover = new XMLCover(xml); trace(cover.object_utils::xml.toXMLString()); // <root>... trace(cover.object_utils::notifier); // [object XMLNotifier] trace(cover.object_utils::createTree); // true var coverList:XMLCoverList = cover.child; trace(coverList.object_utils::list.length()); // 3 trace(coverList.object_utils::notifierList); // [object XMLNotifier],[object XMLNotifier],[object XMLNotifier]
События классов XMLCover/XMLCoverList:
- changed – Событие, сопровождающее любое изменение в XML дереве. Узнать, что точно произошло, можно через свойство changeType объекта XMLEvent.
- attributeAdded – Событие добавления атрибута.
- attributeChanged – Событие изменения значения атрибута.
- attributeRemoved – Событие удаления атрибута.
- nodeAdded – Событие добавления узла.
- nodeChanged – Событие изменения узла, сработает при подмене узла.
- nodeRemoved – Событие удаления узла.
- namespaceAdded – Событие добавления пространства имён.
- namespaceRemoved – Событие удаления пространства имён.
- namespaceSet – Событие установки пространства имён.
- textSet – Событие добавления текстового узла, секции CDATA, если хотите. Если присваивать текст несуществующему узлу, то сначала сработает "nodeAdded".
- nameSet – Событие изменения имени у узла.
xml.@attribute = 'value'; // attributeAdded
xml.@attribute = 'new value'; // attributeChanged
delete xml.@attribute; // attributeRemoved
xml.appendChild(<child/>); // nodeAdded
xml.child = <newChild attribute="1"/>; // nodeChanged
delete xml.child[0]; // nodeRemoved
xml.addNamespace('urn:some-uri'); // namespaceAdded
xml.removeNamespace('urn:some-uri'); // namespaceRemoved
xml.setNamespace('urn:some-uri'); // namespaceSet
xml.child = 'value'; // textSet
xml.child.setLocalName('newChild'); // nameSet
Все события, на которые подписались через объекты XMLCover/XMLCoverList в свойстве "target" будут иметь соответствующий объект XMLNotifier, потому что в этих классах методы описаные в IEventDispatcher переадресовывают вызовы на необходимые объекты XMLNotifier. Константы с типами всех событий находятся в классе XMLEvent.
Дополнительные свойства объекта XMLEvent:
- xmlTarget:Object – Ссылка на XML узел или другой объект XML дерева который вызвал событие.
- currentXmlTarget:Object – Ссылка на результирующий XML узел или другой объект XML дерева, которого коснулись изменения.
- changeType:String – Строка указывающая тип изменения вызвавшего событие.
- value:Object – Использованное значение.
- detail:Object – Дополнительный параметр объект.
В зависимости от события некоторые свойства могут иметь пустые значения.
Пример использования событий:
import aw.xml.XMLCover; import aw.events.XMLEvent; import aw.utils.object_utils; var xml:XML = <root><child id="1"/></root>; var cover:XMLCover = new XMLCover(xml); cover.addEventListener(XMLEvent.NODE_ADDED, this.nodeAddHandler); cover.addEventListener(XMLEvent.NAME_SET, this.nameSetHandler); cover.addEventListener(XMLEvent.CHANGED, this.changedHandler); function nodeAddHandler(e:XMLEvent):void{ trace('nodeAddHandler', e.xmlTarget); } function nameSetHandler(e:XMLEvent):void{ trace('nameSetHandler', e.xmlTarget); } function changedHandler(e:XMLEvent):void{ trace('changedHandler', e.changeType, e.xmlTarget); } cover.child.appendChild(<grandchild/>); /* nodeAddHandler <child id="1">... changedHandler nodeAdded <child id="1"> */ xml.appendChild(<child id="2"/>); /* nodeAddHandler <root>... changedHandler nodeAdded <root>... */ var node:XML = xml.child.(@id=="2")[0]; node.setName(new QName('urn:some-uri', 'renamedChild')); /* nameSetHandler changedHandler nameSet */ node.removeNamespace(new Namespace('urn:some-uri')); /* changedHandler namespaceRemoved */
Пример биндинга:
<?xml version="1.0" encoding="utf-8"?> <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical" creationComplete="creationCompleteHandler();"> <mx:Script> import flash.utils.setTimeout; import aw.xml.XMLCover; public var xml:XML = <root><child attr="some value"/></root>; public var cover:XMLCover = new XMLCover(xml); public function creationCompleteHandler():void{ setTimeout(resetValue, 3000); } public function resetValue():void{ this.cover.child.@attr = 'Attribute value changed'; lblInfo.text = 'changed'; } </mx:Script> <mx:Label text="{cover.child.@attr}"/> <mx:Label text="" color="0xff0000" id="lblInfo"/> </mx:Application>
Пример биндинга, использование тега mx:Binding:
<?xml version="1.0" encoding="utf-8"?> <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical" creationComplete="creationCompleteHandler();"> <mx:Script> import flash.utils.setTimeout; import aw.xml.XMLCover; public var xml:XML = <root><child attr="some value"/></root>; public var cover:XMLCover = new XMLCover(xml); public function creationCompleteHandler():void{ setTimeout(resetValue, 3000); } public function resetValue():void{ this.cover.child.@attr = 'Attribute value changed'; lblInfo.text = 'changed'; } </mx:Script> <mx:Binding source="cover.child.@attr" destination="lblAttributeValue.text"> </mx:Binding> <mx:Label text="" id="lblAttributeValue"/> <mx:Label text="" color="0xff0000" id="lblInfo"/> </mx:Application>
Пример биндинга, использование модели:
<?xml version="1.0" encoding="utf-8"?> <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical" creationComplete="creationCompleteHandler();"> <mx:Script> import aw.events.XMLEvent; import mx.binding.utils.BindingUtils; import flash.utils.setTimeout; import aw.xml.XMLCover; public var xml:XML = <root></root>; public var cover:XMLCover = new XMLCover(xml); public function creationCompleteHandler():void{ this.cover.appendChild(<child attr="some value"/>); BindingUtils.bindProperty(this.lblAttributeValue, 'text', this.bindingModel, 'bind'); setTimeout(resetValue, 3000); } public function resetValue():void{ this.cover.child.@attr = 'Attribute value changed'; lblInfo.text = 'changed'; } </mx:Script> <mx:Model id="bindingModel"> <Model> <bind>{this.cover.child.@attr}</bind> </Model> </mx:Model> <mx:Label text="" id="lblAttributeValue"/> <mx:Label text="" color="0xff0000" id="lblInfo"/> </mx:Application>
Пример биндинга, использование класса BindingUtils:
<?xml version="1.0" encoding="utf-8"?> <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical" creationComplete="creationCompleteHandler();"> <mx:Script> import aw.events.XMLEvent; import mx.binding.utils.BindingUtils; import flash.utils.setTimeout; import aw.xml.XMLCover; public var xml:XML = <root></root>; public var cover:XMLCover = new XMLCover(xml); public function creationCompleteHandler():void{ this.cover.appendChild(<child attr="some value"/>); BindingUtils.bindProperty(this.lblAttributeValue, 'text', this.cover.child[0], '@attr'); setTimeout(resetValue, 3000); } public function resetValue():void{ this.cover.child.@attr = 'Attribute value changed'; lblInfo.text = 'changed'; } </mx:Script> <mx:Label text="" id="lblAttributeValue"/> <mx:Label text="" color="0xff0000" id="lblInfo"/> </mx:Application>
Ложка дёгтя:
- Если использовать дерево XMLCover для обращения к дочерним XML узлам и их свойствам, то скорость работы с E4X падает в 6-12 раз.
- Во Flex Builder 3 появляются варнинги при использовании биндинга через MXML синтаксис. Он просто не может обнаружить динамически создаваемые свойства.
- При использовании объектов XML с классом Dictionary наткнулся на странную особенность класса Dictionary, которую посчитал багом. Поэтому для поиска нужного узла в Dictionary, воспользовался цыклом for…in, который тоже влияет на производительность.
Метки: Bindable, flash.events.EventDispatcher, flash.utils.Proxy, XML, XML.setNotification