PDA

Просмотр полной версии : AS-алхимия: обработка события с доп. параметрами.


WindWalker
27.04.2007, 16:44
Очень многие новички, более-менее разобравшись с конструкцией типа
button.onRelease = onButtonRelease;
зачастую встают в ступор, когда требуется передать дополнительные параметры.

Предположим, что у нас есть что-то вроде меню, которое состоит из трёх кнопок. И нам нужно сделать одну и ту же функцию-обработчик для всех трёх кнопок, но чтобы у этой функции был параметр, определящий, какая именно кнопка нажата.

Большинство тех, кто делает первые шаги в изучении AS, первым делом пытается сделать что-то вроде такого:
btnMenu1.onRelease = this.onMenuReleased(1);
btnMenu2.onRelease = this.onMenuReleased(2);
btnMenu3.onRelease = this.onMenuReleased(3);

С точки зрения программирования - полнейшая чушь.
Но с другой стороны - интуитивно-понятная вещь (не зря же так много людей абсолютно независимо друг от друга совершает одну и ту же ошибку).

Класс mx.utils.Delegate не позволяет передавать дополнительные параметры. К счастью, его можно самостоятельно слегка модицифировать.
Тогда можно будет написать примерно так:
btnMenu1.onRelease = Delegate.create(this, this.onMenuReleased, 1);
btnMenu2.onRelease = Delegate.create(this, this.onMenuReleased, 2);
btnMenu3.onRelease = Delegate.create(this, this.onMenuReleased, 3);

Выглядит это крайне громоздко. Кроме того, приходится дублировать объект, обрабатывающий событие - в первом и во втором параметре.
И если this зачастую ещё можно и опустить, то если там нужно указать какой-нить конкретный объект (например, какой-нибудь menuHandler), то дублирование будет обязательным.

Я долгое время мечтал сделать способ задавать обработчки событий максимально близко к тому самому ошибочному, но интуитивно-понятному способу.

Теперь у меня код имеет такой стиль:
btnMenu1.onRelease = new Call(this).onMenuReleased(1);
btnMenu2.onRelease = new Call(this).onMenuReleased(2);
btnMenu3.onRelease = new Call(this).onMenuReleased(3);

Совсем чуть-чуть лишней мишуры и ошибочный подход стал вполне работоспособным.

Всё волшебство находится в классе Call.
Это настоящая алхимия и херомантия в одном флаконе.
Вот так выглядит класс:
dynamic class as.common.Call {
private var __oScope:Object;
private var __fFunction:Function;
private var __aArguments:Array;

public function Call(oScope:Object) {
__oScope = oScope;
}

public function __resolve(sFunctionName) {
__fFunction = __oScope[sFunctionName];
return __prepareCall;
}

private function __prepareCall() {
var f = function() {
var scope = arguments.callee.oScope;
var func = arguments.callee.fFunction;
return func.apply(scope, arguments.concat(arguments.callee.aArguments));
};
f.oScope = __oScope;
f.fFunction = __fFunction;
f.aArguments = arguments;
return f;
}
}

Довольно короткий, но без бутылки не разберёшься :)

Итак, как он работает.
Разберём по порядку
new Call(this).onMenuReleased(1);

Выражение new Call(this) создает новый экземпляр класса Call, который сохраняет ссылку на объект-исполнитель в поле __oScope.

new Call(this).onMenuReleased(1);

Указанный затем метод (в данном случае - onMenuReleased) как бы относится уже к созданному экземпляру класса Call.
Но такого метода у этого класса нет, поэтому вызывается метод __resolve, который переопределён.
Этот метод находит по имени нужный метод в объекте-исполнителе (здесь делается предположение, что именно там и надо его искать), запоминает его в поле __fFunction, а возвращает ссылку на метод __prepareCall.

new Call(this).onMenuReleased(1);

Скобочки после названия метода означают, что его надо исполнить.
Однако исполняется у нас не onMenuReleased, а __prepareCall, ссылка на который была возвращена.

Этот метод делает последний штрих - берёт переданные параметры и создаёт специальную функцию (f), которая умеет уже запускать нужный нам метод от имени нужного объекта с нужными параметрами.
И именно эта специальная функция и присваивается в onRelease.

Таким образом, в реальности у нас происходят куда более хитрые процессы, чем это кажется на первый взгляд.
Но если воспринимать класс Call как "чёрный ящик", то использовать его крайне легко, и, думаю, многим новичкам он очень пригодится.

etc
27.04.2007, 16:46
Дяденька, вы маленько опоздали. Все, кому надо было, уже лет пять назад такое сделали… Уж тем более __resolve тут нафиг не нужен, не говоря уже о том, что это не есть правильно.

screamge
27.04.2007, 16:48
class ru.inils.util.Delegate extends Object {
/**
* Передача событий от любого объекта к любому объекту.
*
* @usage public static create (obj:Object, func:Function, arg:Array) : Function
* @param obj Object - Объект, целевая область видимости.
* @param func Function - Метод, обработчик данного объекта.
* @param arg Array [дополнительный параметр] - Массив передаваемых аргументов.
* @return Function - Функция обработчик.
*/
public static function create (obj:Object, func:Function, arg:Array):Function {
var f = function () {
var targetTemp = arguments.callee.target;
var funcTemp = arguments.callee.func;
var argTemp = arguments.callee.arg;
return funcTemp.apply (targetTemp, argTemp);
};
f.target = obj;
f.func = func;
f.arg = arg;
return f;
}
}

Класс написанный iNils, передавайте параметры легко и просто.

WindWalker
27.04.2007, 16:49
__etc

Без __resolve будет выглядеть не так красиво.

screamge

О чём-то подобном я и говорил, когда писал про "немного модифицировать Delegate".
В вашем варианте вижу две проблемы:
1. Аргументы надо передавать массивом, что не очень красиво смотриться.
2. Некоторые события сами имеют параметры (например, killFocus), в вашем варианте они потеряются.

etc
27.04.2007, 16:53
Вам шашечки или ехать? Ради сомнительной красоты кода использовать dynamic и __resolve — перебор.
А что будет, если мы сделаем подобный делегейт в такой схеме:

addEventListener('anyEvent',{anyEvent:new Call(this).anyEventHandler(123)}
event-объект в первом аргументе мы, скорее всего, не получим. Сейчас проверим.

WindWalker
27.04.2007, 16:56
Согласен, компилятор перестанет находить ошибки.
Но в случае использования модифицированного Delegate тоже нет проверки, скажем, типов параметров.

А код действительно становится гораздо нагляднее и понятнее (если, конечно, не глядеть внутрь Call).

P.S.
А я знал, что __etc'у не понравится :)

P.P.S.
Перенесите в раздел для новичков :)


event-объект в первом аргументе мы, скорее всего, не получим. Сейчас проверим.

Получим-получим!
Не зря же у меня вот такое:
arguments.concat(arguments.callee.aArguments)

etc
27.04.2007, 17:06
Да, получим, проверил уже.
Тем не менее, я уже говорил, что dynamic и __resolve в данном случае зло и применяется ради очень сомнительной выгоды. Я уже не говорю про то, что может быть непонятного в строках:

btnMenu1.onRelease = Delegate.create(this, this.onMenuReleased, 1);
?
Начинающих класс Call вообще сведет в полный тупик, нежели Delegate, потому как им придется выяснять, что это за __resolve, dynamic и т.п. А продолжающим вторая строка понятна абсолютно.

WindWalker
27.04.2007, 17:21
Да всё понятно.
Я сам подобной штукой долгое время пользовался (только называлось у меня Tools.delegate).

Но напрягало глаза и руки:
1. Дважды указывается объект-исполнитель.
2. Метод не похож на метод, а просто какой-то параметр. А когда таких делегейтов много, начинает пестрить.
3. Параметры идут как бы особняком, а не в скобочках после метода.

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

iNils
27.04.2007, 17:24
Встану на защиту своих аргументов в модифицированом Delegate :) Можно конечно и не массивом их передавать, написать дополнительных пару строку не проблема. Но массив позволяет нам передавать аргументы, где-то заранее сформированные или динамические. Поэтому дело тут совсем не в красоте, а в практическом использовании.

screamge
27.04.2007, 17:25
максимально интуитивно понятным и тот и другой способ является для разбирающегося хорошо в кодинге, новичку сложнее будет разобраться с call так как он редко где встречается, уж поверьте мне :)

etc
27.04.2007, 17:26
Второй раз объекта исполнителя можно не указывать, если с областями видимости все в порядке. Лично я всегда указываю — привычка.

iNils
27.04.2007, 17:27
Я так понял, весь сыр бор в том, что WindWalker не нравится внешний вид
btnMenu1.onRelease = Delegate.create(this, this.onMenuReleased, 1);А по душе
btnMenu1.onRelease = new Call(this).onMenuReleased(1);
Все из-за пары букв лишних в коде?

etc
27.04.2007, 17:31
iNils, можно твой Delegate сократить до:

public static function create(obj:Object, func:Function, arg:Array):Function {
return function () {
return func.apply(obj, arguments.concat(arg));
};
}

:D
Правда, в ущерб некоторой функциональности (почти незаметный).

WindWalker
27.04.2007, 17:37
Все из-за пары букв лишних в коде?
Скорее, в их порядке.
Ну представьте, что писали бы на AS вот так:
invoke moveTo, mc, 0, 0
invoke lineTo, mc, 0, 100
...
Красиво? :)

etc
27.04.2007, 17:39
Лично меня такое написание нисколько не смущает.

WindWalker
27.04.2007, 17:49
Кстати, можно же и вообще без классов обойтись:

var handler = this;
btnMenu1.onRelease = function() { handler.onMenuReleased(1) }
btnMenu2.onRelease = function() { handler.onMenuReleased(2) }
btnMenu3.onRelease = function() { handler.onMenuReleased(3) }
Тоже совсем немножко лишник буковок.
Но лично меня всегда смущало, что локальная переменная оказывается какой-то не совсем локальной.
Это уже совсем крутое колдунство.

iNils
27.04.2007, 17:49
Скорее, в их порядке.
Ну представьте, что писали бы на AS вот так:
invoke moveTo, mc, 0, 0
invoke lineTo, mc, 0, 100
...
Красиво? :)А причем тут текущий синтаксис Delegate? Там идет объект, метод, аргументы. Точно также как и у вас и в setInterval.

WindWalker
27.04.2007, 17:55
Скобочки стоят не совсем там, где надо.
И объект отделается от метода не точкой, а запятой (если опускать this).

А setInterval вообще жуткая весчь, я бы не стал её брать как образец для подражания :)

iNils
27.04.2007, 17:57
Скобочки стоят не совсем там, где надо.
И объект отделается от метода не точкой, а запятой (если опускать this).Это то, ради чего вы это все затеяли?

Kikasso
27.04.2007, 17:59
function onMenuReleased( param ){
return ( function(){ trace("hello from " + this._name); trace(param); } );
}

btnMenu1.onRelease = onMenuReleased(1);
btnMenu2.onRelease = onMenuReleased(2);
btnMenu3.onRelease = onMenuReleased(3);

а насчет Call.. Если вспомнить что это раньше обозначало..

Или вообще вот так:
function onMenuReleased(){
var arr = arguments;
return ( function(){ trace("hello from " + this._name); trace(arr); } );
}

btnMenu1.onRelease = onMenuReleased(1);
btnMenu2.onRelease = onMenuReleased(2);
btnMenu3.onRelease = onMenuReleased(3,4,5);

И всё. Никаких тебе классов.

iNils
27.04.2007, 18:31
Kikasso, так у нас же область видимости тогда не меняется. Как были внутри кнопки, так в ней и остались.

WindWalker
27.04.2007, 19:23
Это то, ради чего вы это все затеяли?
Да. И в первом сообщении я довольно подробно это объяснил.

Kikasso
У вас this получается Button.

iNils
27.04.2007, 19:38
Да. И в первом сообщении я довольно подробно это объяснил.Не может я чего то не понял. :D А так вы изобрели велосипед, только колеса у вас квадратные.

WindWalker
27.04.2007, 20:00
Нет, мой велосипед выглядит вот так:
static function delegate(obj: Object, func: Function):Function {
var f = function() {
var target = arguments.callee.target;
var func = arguments.callee.func;
return func.apply(target, arguments.concat(arguments.callee.args));
};
f.target = obj;
f.func = func;
f.args = arguments.slice(2);
return f;
}
На нём и катался последние два-три года.

А класс Call - это чистая магия :)

iNils
27.04.2007, 20:06
WindWalker, есть один такой здоровый минус. Когда я пользуюсь в FlashDevelop своим Delegate, там у меня работает автокомплит. И здесь его не будет.

Kikasso
27.04.2007, 20:30
Зачем нам колдовать с областью видимости? Мы можем передать ее параметром, а уж что за функция будет "на приеме", другое дело:
function onMenuReleased(){
var arr = arguments;
return ( function(){ trace("hello from " + this._name); trace( "arguments: " + arr); } );
}
btnMenu1.onRelease = onMenuReleased(1);
btnMenu2.onRelease = onMenuReleased(2);
btnMenu3.onRelease = onMenuReleased(this, 3, 4);
И наконец, почему бы новичкам не научиться писать нормально?
function onMenuReleased(){
trace("hello from " + this);
trace( arguments.length ? ("arguments: " + arguments.join(", ") ) : "no arguments" );
}
btnMenu1.onRelease = onMenuReleased;
btnMenu2.onRelease = function(){ onMenuReleased(2); }
btnMenu3.onRelease = function(){ onMenuReleased(this, 3, 4); }
btnMenu4.onRelease = function(){ onMenuReleased(); }

etc
27.04.2007, 21:37
Область видимости не должна меняться.

WindWalker
28.04.2007, 00:43
И наконец, почему бы новичкам не научиться писать нормально?
function onMenuReleased(){
trace("hello from " + this);
trace( arguments.length ? ("arguments: " + arguments.join(", ") ) : "no arguments" );
}
btnMenu1.onRelease = onMenuReleased;
btnMenu2.onRelease = function(){ onMenuReleased(2); }
btnMenu3.onRelease = function(){ onMenuReleased(this, 3, 4); }
btnMenu4.onRelease = function(){ onMenuReleased(); }

Тут крайне неоднозначно будет с видимостями.

Сделал я 4 кнопочки, поместил этот код в кадр.
Затем запустил и поочередно щёлкнул по каждой кнопке.
И вот что я получил:
hello from _level0.btnMenu1
no arguments
hello from _level0
arguments: 2
hello from _level0
arguments: _level0.btnMenu3, 3, 4
hello from _level0
no arguments

Что вполне логично.
В первом случае onMenuReleased вызвалось от имени кнопки.
Во всех остальных случаях неявно подставился _root.

Затем я объединил все кнопки в один мувик и привязал к этому мувику вот такой вот класс:

class Menu extends MovieClip {
var btnMenu1:Button;
var btnMenu2:Button;
var btnMenu3:Button;
var btnMenu4:Button;

function onMenuReleased(){
trace("hello from " + this);
trace( arguments.length ? ("arguments: " + arguments.join(", ") ) : "no arguments" );
}

function Menu() {
btnMenu1.onRelease = onMenuReleased;
btnMenu2.onRelease = function(){ onMenuReleased(2); }
btnMenu3.onRelease = function(){ onMenuReleased(this, 3, 4); }
btnMenu4.onRelease = function(){ onMenuReleased(); }
}
}
Т.е. практически тоже самое.
Однако теперь работает только первая кнопка (и опять же this будет кнопка, а не наш класс).
Во всех остальных случаях он не может найти onMenuReleased.


Вот как раз из-за таких проблем с областью видимости и придумали класс mx.utils.Delegate, который каждый переделывает на свой лад.

etc
28.04.2007, 00:46
Не надо объявлять методов в методах, от этого только проблемы возникнут. А в Delegate лучше не смотреть, работает и работает. Это костыль в AS2, без него никуда, увы.

Kikasso
28.04.2007, 01:55
Во всех остальных случаях он не может найти onMenuReleased
Конечно, он не может его найти. this в этот момент вообще не пойми что, btnOnRelease тоже как таковой нет, а вы создаете метод с ее участием.
Если объявлять эту btnOnrelease в конструкторе, это сработает, но без ссылки на самого себя не обойтись.
Только такой изврат извратович:
class Menu extends MovieClip {
var btnMenu1:Button;
var btnMenu2:Button;
var btnMenu3:Button;
var btnMenu4:Button;
function onMenuReleased():Void{
trace("hello from " + this);
trace( arguments.length ? ("arguments: " + arguments.join(", ") ) : "no arguments" );
}
function Menu() {
var viz:Menu = this;
btnMenu1.onRelease = onMenuReleased;
btnMenu2.onRelease = function(){ viz.onMenuReleased(2); }
btnMenu3.onRelease = function(){ viz.onMenuReleased(this, 3, 4); }
btnMenu4.onRelease = function(){ viz.onMenuReleased(); }
}
}
Жуть.
__etc прав, не надо мудрить.
А если без классов, в кадре - проблем нет.