Reification for bindx

Доброго времени суток, %haxe_user%! Эта статью, с определенным допущением, можно назвать долгожданным продолжением первой статьи о связавании данных в хаксе.

С выхода библиотеки bindx прошло много времени и пришло время доработать, а точнее даже переработать всю библиотеку. Изменений настолько много и они настолько глобальные, что решил написать подробную статью. Отсюда и такое название самой статьи. Но обо всем по порядку.

Первое, что сразу бросится в глаза - это новая система сигналов. Если раньше для всех полей создавался единый диспатчер, то теперь создается сигнал для каждого поля. Например для поля

@:bindable public var foo:String

Автоматически создастся сигнал fooChanged, на который можно сразу подписаться:
fooChanged.add(function (from, to) {});

Никаких больше Bind.bindx, хотя они и остались.

Нужно больше сигналов

Но и тут надо зайти издалека. Все началось с того, что меня попросили поддержать стороние сигналы в библиотеке, что и было реализованно в новой версии. В этом есть смысл и поэтому появился bindx.IBindingSignalProvider, реализовать который не так уж просто, но зато у вас могут появиться данные связанные с вашими сигналами и дженериками. Из коробки поставляются максимально простые сигналы, но для 99% случаев их должно хватить. Базовые сигналы поддерживают несколько важных настроек:

  • lazySignal - ленивая инициализация сигналов. Сигналы создаются при первом обращении к геттеру сигнала (по умолчанию включена). Выглядит это примерно так:
    var _fooChanged:FieldSignal<String>;
    public var fooChanged(get, never):FieldSignal<String>;
    function get_fooChanged()  {
                    if ((this._fooChanged == null))
    this._fooChanged = new bindx.FieldSignal<String>());
                    return this._strChanged;
            }

    Если отключить ленивую инициализацию - сигналы будут создаваться сразу.
  • inlineSignalGetter - добавление inline геттеру сигнала. Актуально только в случае включенной lazySignal настройки (по умолчанию отключено).

Итого у нас есть два способа инициализации сигналов и в случае создания геттера сигнала, мы можем сделать его инлайновым. Если у вас будут и другие пожелания, я готов их обсудить и по возможности реализовать.

Теперь, когда я немного рассказал про "поставщика сигналов" и базовые сигналы, стоит объяснить зачем остались Bind.bindx. Дело в том, что код вида fooChanged.add “сигнало зависим”, и при смене сигналов он возможно перестанет работать, в то время как Bind.bindx не поменяет своего апи и учитывает lazySignal параметр. Что использовать - решать вам. Главное, что я вас предупредил :). Не стоит забывать и о Bind.unbindx имеющем обратный эффект, но при этом таком же полезном.

Еще один очень полезный метод Bind.bindTo, позволяющем связывать одно свойство с другим и возвращающем метод для отписки:

var unbind = bindx.Bind.bindTo(vo.score, label.text);

А как же методы?

Подписка на методы выполняется аналогично, а оповещать об изменениях методов можно напрямую, вызывая метод dispatch у сигнала или используя утилитарный класс bindx.Bind.notify(this.toString). И снова важно понимать, что если вы вызовете напрямую dispatch у сигнала, то в случае ленивой инициализации сигнала, вы автоматически его создадите, в то время, когда notify все это учитывает. Диспатчить напрямую сигналы я вообще не советовал бы, но, зная программистов, даже не стал делать метод приватным - не поможет. Bind.notify умеет оповещать об измененниях и обычных свойств:

bindx.Bind.notify(b.str, "1", "2");

bindx.IBindable

Если вы еще не открывали первую статью про bindx первой версии, то самое время это сделать (Bindx intro). Потому что далее пойдет речь об интерфейсе bindx.IBindable, а его поведение во многом сохранилось. Он все также для всех переменных создает приватные сеттеры с диспатчингом об изменении. Он все так же патчит уже существующие сеттеры, добавляя перед самым возвратом функции вызов сигнала, а в начале метода дополнительную проверку на то, что новое значение действительно отличается от предыдущего (если что-то из этого вам только мешает, используйте force параметр).

@:bindable мета поддерживает два дополнительных параметра:

  • force - не будет модифицироваться сеттер и не создаст никаких новых сеттеров, появится только сигнал. А вот оповещение об изменении полностью на вас. Очень полезное свойство, позволяет полностью управлять оповещением об изменении свойства и предотвратить автоматическую модификацию сеттера (по умолчанию отключено).
  • inlineSetter - актуально только для переменных без явно указанного сеттера (var foo:Bar), автосгенерированный сеттер будет инлайн (по умолчание отключено).

Стоит напомнить и правила @:bindable метаданных. Что подвергнется обработке:

  • всех полей и методов с метатегом @:bindable (кроме конструктора и статических),
  • всех публичных полей с сеттером default или set если у самого класса указан метатег @:bindable. Тут очень важно указать, что все атрибуты указанные в мете класса, автоматически скопируются всем полям из данной группы. Например указав классу @:bindable(inlineSignalGetter=true) вы автоматом включите это свойство всем полям у которых нет своей меты @:bindable. Такое себе наследование, правда не передается дочерним классам.

Параметр force уменьшает требования для полей и позволяет связывать данные даже без сеттера (то есть один из вариантов: never, null, dynamic).

Классы реализующие интерфейс IBindable можно наследовать - все дочерние классы будут отлично работать и также поддерживать мету @:bindable.

Пример

Напоследок небольшой пример для наглядности с пояснениями.

class Example implements bindx.IBindable {

    public function new() {}

    @:bindable(lazySignal=true, inlineSignalGetter=false, inlineSetter=true)
    public var foo:String;

    @:bindable(force=true)
    public var bar(default, set):Int;

    function set_bar(v):Int {
        if (v ==bar) return v;
        bindx.Bind.notify(this.bar, bar, bar = v);
        return v;
    }
}

Ну а теперь, немного разберем сам пример:

Переменная foo обзаведется инлайновым сеттером inlineSetter=true. Геттер сигнала не будет инлайновым inlineSignalGetter=false, но в нем будет инициирован сигнал при первой необходимости lazySignal=true. Как lazySignal=true так и inlineSignalGetter=false можно было не указывать, так как это их поведение по умолчанию.

Сеттер переменной bar вообще не будет модифицироваться force=true, лишь появится сигнал с ленивой инициализацией. Именно по этому мы вручную вызываем Bind.notify(this.bar) внутри сеттера bar.

Ну а дальше можно просто подписываться на сигналы или, опять-таки, использовать bindx.Bind:

var v = new Example();
bindx.Bind.bindx(v.foo, function (from, to) {trace('foo changed from $from to $to');});
v.fooChanged.add(function (from, to) {trace('foo changed from $from to $to');});
var unbind = bindx.Bind.bindTo(v.bar, label.text);

unbind();

Заключение

На данный момент библиотека все еще развивается, и хочется получить как можно больше фидбека. Если вы использовали предыдущую версию bindx или вот-вот собирались это сделать, советую попробовать новую библиотеку. Установить ее можно с помощью команды:

haxelib git bindx2 https://github.com/profelis/bindx2.git

Отдельная благодарность Александру Козловскому, Харват Егору, Сергею Драган и Сергею Мирянову за помощь в написании статьи.