I've got another "super" testcase for you, which shows again
differences between queued and non-queued delegates...I guess you're
starting to hate me...
testcase:
[code]
<canvas debug="true" >
<class name="test" extends="node" >
<attribute name="foo" value="000" type="string"
setter="setFoo(foo)" />
<attribute name="bar" value="aaa" type="string"
setter="setBar(bar)" />
<event name="onfoo" />
<event name="onbar" />
<handler name="onfoo" args="foo" >
Debug.write("onfoo with %s", foo);
this.setBar(this.bar + foo);
</handler>
<handler name="onbar" args="bar" >
Debug.write("onbar with %s", bar);
this.setFoo(this.foo + bar);
</handler>
<method name="setFoo" args="foo" ><![CDATA[
Debug.write("setFoo with %s", foo);
this.foo = foo;
this.onfoo.sendEvent(foo);
]]></method>
<method name="setBar" args="bar" ><![CDATA[
Debug.write("setBar with %s", bar);
this.bar = bar;
this.onbar.sendEvent(bar);
]]></method>
</class>
<test id="foobar" >
<!-- later -->
<handler name="oninit" reference="canvas" >
Debug.write("-----");
this.foo = "";
this.bar = "";
this.setBar("aaa");
this.setFoo("000");
</handler>
</test>
</canvas>
[/code]
i) current output (without your changeset applied):
setBar with aaa
setFoo with 000
onbar with aaa
setFoo with 000aaa
onfoo with 000aaa
setBar with aaa000aaa
onbar with aaa000aaa
setFoo with 000aaaaaa000aaa
onfoo with 000
setBar with aaa000aaa000
onbar with aaa000aaa000
setFoo with 000aaaaaa000aaaaaa000aaa000
onfoo with 000aaaaaa000aaaaaa000aaa000
setBar with aaa000aaa000000aaaaaa000aaaaaa000aaa000
-----
setBar with aaa
onbar with aaa
setFoo with aaa
onfoo with aaa
setBar with aaaaaa
setFoo with 000
onfoo with 000
setBar with aaaaaa000
onbar with aaaaaa000
setFoo with 000aaaaaa000
ii) output with your changeset:
setBar with aaa
setFoo with 000
onbar with aaa
setFoo with 000aaa
onfoo with 000aaa
setBar with aaa000aaa
onfoo with 000
setBar with aaa000aaa000
-----
setBar with aaa
onbar with aaa
setFoo with aaa
onfoo with aaa
setBar with aaaaaa
setFoo with 000
onfoo with 000
setBar with aaaaaa000
onbar with aaaaaa000
setFoo with 000aaaaaa000
iii) output of a prototype of mine (see attachment)
setBar with aaa
setFoo with 000
onbar with aaa
setFoo with 000aaa
onfoo with 000aaa
setBar with aaa000aaa
onfoo with 000
setBar with aaa000aaa000
onbar with aaa000aaa000
setFoo with 000aaaaaa000aaa000
-----
setBar with aaa
onbar with aaa
setFoo with aaa
onfoo with aaa
setBar with aaaaaa
setFoo with 000
onfoo with 000
setBar with aaaaaa000
onbar with aaaaaa000
setFoo with 000aaaaaa000
Summary:
i) => too much function calls (+4)
ii) => function calls are missing (-2)
iii) => function calls match
BUT, what do we actually expect for queued delegates?
On 11/30/2007 6:41 PM, P T Withington wrote:
I updated the change to correctly handle your new test case. Would
you like to re-review it (http://svn.openlaszlo.org/openlaszlo/patches/20071105-ptw-V.tar
)
I 'improved' your test case just a bit to make more sense to me:
<canvas debug="true" >
<class name="foo" extends="node" >
<attribute name="bar" value="123" setter="setBar(bar)" />
<method name="setBar" args="bar" >
this.bar = bar;
if (this["onbar"]) {
this.onbar.sendEvent(bar);
}
</method>
<method name="provokeError" >
this.setBar("789");
</method>
<method name="provokeError2" >
this.setBar("xxx");
</method>
</class>
<foo bar="456" >
<handler name="onbar" args="bar">
Debug.write("onbar - 1 - begin with %s", bar);
this.provokeError();
Debug.write("onbar - 1 - end with %s", this.bar);
</handler>
<handler name="onbar" args="bar">
Debug.write("onbar - 2 - begin with %s", bar);
this.provokeError2();
Debug.write("onbar - 2 - end with %s", this.bar);
</handler>
<!-- later -->
<handler name="oninit" reference="canvas" >
Debug.write("-----");
this.setBar("abc");
</handler>
</foo>
</canvas>
On 2007-11-06, at 19:28 EST, André Bargull wrote:
Approved.
I've got another testcase which shows a different event-handling
(comparing queued and non-queued delegates),
even after applying this fix, but I don't know whether it is a bit
too much constructed:
<canvas debug="true" >
<class name="foo" extends="node" >
<attribute name="bar" value="123" setter="setBar(bar)" />
<method name="setBar" args="bar" >
this.bar = bar;
if (this["onbar"]) {
this.onbar.sendEvent(bar);
}
</method>
<method name="provokeError" >
this.setBar("789");
</method>
<method name="provokeError2" >
this.setBar("xxx");
</method>
</class>
<foo bar="456" >
<handler name="onbar" args="bar">
Debug.write("onbar - 1 - begin with %s", bar);
this.provokeError();
Debug.write("onbar - 1 - end with %s", bar);
</handler>
<handler name="onbar" args="bar">
Debug.write("onbar - 2 - begin with %s", bar);
this.provokeError2();
Debug.write("onbar - 2 - end with %s", bar);
</handler>
<!-- later -->
<handler name="oninit" reference="canvas" >
Debug.write("-----");
this.setBar("abc");
</handler>
</foo>
</canvas>
On 11/7/2007 12:23 AM, P T Withington wrote:
Change 20071105-ptw-V by [EMAIL PROTECTED] on 2007-11-05
15:10:35 EST
in /Users/ptw/OpenLaszlo/ringding-2
for http://svn.openlaszlo.org/openlaszlo/trunk
Summary: Ensure deferred events do not recurse
Bugs Fixed:
LPP-4950 'LzDelegate single-execution mechanism does not work for
queued delegates'
Technical Reviewer: hminksy (pending)
QA Reviewer: [EMAIL PROTECTED] (pending)
Tests:
Test case from bug no longer shows the event recursing.
Files:
M WEB-INF/lps/lfc/events/LaszloEvents.lzs
Changeset: http://svn.openlaszlo.org/openlaszlo/patches/20071105-ptw-V.tar
/**
*
* @copyright Copyright 2001-2007 Laszlo Systems, Inc. All Rights
Reserved.
* Use is subject to license terms.
*
* @affects lzevent lzdelegate
* @topic LFC
* @subtopic Events
* @access public
*/
/**
* <p>
* A delegate is an object that calls a specific method in a
specific context
* when its execute method is called. It is essentially a function
pointer.
* </p>
* <p>
* Delegates, along with <xref linkend="LzEvent"/>, comprise
Laszlo's point to point event system. A delegate represents a named
method of an instance. Delegates are mostly registered with events,
but they can be used anywhere a function callback would
traditionally be called for: for instance, the <xref
linkend="LzTimer"/> service takes a delegate as its argument when a
timer is started. See the code example on the <xref
linkend="LzEvent"/>.
* </p>
*
* @shortdesc The receiver in Laszlo's point-to-point event system.
* @see LzEvent
*/
class LzDelegate {
/**
*
* @param Object context: reference to object which will be called
* @param String functionName: name of the method to call (a string)
* @param Object eventSender: optional; the sender of the event to
register the
* new delegate for.
* @param String eventName: Optional, but required if eventSender is
used; The name
* of the event to register the new delegate for.
*/
function initialize (context, functionName, eventSender, eventName) {
super.initialize.apply(this, arguments);
// too expensive to leave on all the time
// if ($debug) {
// this._dbg_created = Debug.backtrace();
// }
this.c = context;
if ( $debug ){
if (typeof(functionName) != "string")
Debug.warn('LzDelegate functionName must be a string in %w',
arguments.caller);
}
this.f = functionName;
if ( eventSender != null ){
if ( $debug ) {
if (typeof(eventName) != "string") {
Debug.warn('LzDelegate eventName must be a string in %w',
arguments.caller);
}
}
this.register( eventSender , eventName );
}
this.__delegateID = LzDelegate.__nextID++;
}
/** @access private */
static var __nextID = 1;
/** The context in which to call the method
* @type Object
*/
var c;
/** The name of the method to call
* @type String
*/
var f;
var lastevent = 0;
var enabled = true;
var event_called = false;
/**
* Executes the named method in the given context with the given
data. Returns
* the result of the call. In rare cases, this method may be
overriden by a
* subclass, and so it is marked 'protected' instead of 'private'.
*
* @param sd: The data with which to call the method.
* @return: The value returned by the method call.
* @access protected
*/
function execute (sd){
// Don't execute if context has been deleted, as that could
// 'resurrect' the deleted view, causing a memory leak
// Usually this is because a deleted view has idle or timer events
// still registered.
var context = this.c;
if (context) {
if (context['__LZdeleted']) {
return;
}
return context[this.f]( sd );
}
}
/**
* Registers the delegate for the named event in the given context.
<b>NB:</b>
* This is the primary way in which events are created. Published
events do
* not generally exist as objects (with some exceptions) until
delegates are
* created for them.
*
* @param Object eventSender: The object which publishes the event.
* @param String eventName: The name (string) of the event to
register for.
*/
function register ( eventSender , eventName){
if (! eventSender) {
if ($debug) {
Debug.error('No eventSender (%w) for %s', eventSender,
eventName);
}
return;
}
var anEvent = eventSender[ eventName ];
if ( anEvent == LzDeclaredEvent || !anEvent ){
// The call used to be unrolled here for swf. I removed it
since it
// was done during the swf5 days
anEvent = new LzEvent( eventSender, eventName , this );
} else {
anEvent.addDelegate( this );
}
this[ this.lastevent++ ] = anEvent;
// If debugging, add an informative name
if ($debug) {
var anEvent = eventSender[ eventName ];
if (! anEvent.hasOwnProperty('_dbg_eventSender')) {
anEvent._dbg_eventSender = eventSender;
anEvent._dbg_eventName = eventName;
// too expensive to leave on all the time
// anEvent._dbg_created = Debug.backtrace();
}
}
if ($profile) {
var anEvent = eventSender[ eventName ];
if (! anEvent.hasOwnProperty('_dbg_profileName')) {
anEvent._dbg_profileName = eventName;
}
}
}
/**
* Unregisters the delegate for all of the events it is registered
for.
*/
function unregisterAll ( ){
for (var i = 0; i< this.lastevent ; i++){
this[ i ].removeDelegate( this );
this[ i ] = null;
}
this.lastevent = 0;
}
/**
* Unregisters the delegate for the given event
* @param LzEvent event: The event to unregister the delegate from.
* (e.g. myview.onmouseup)
*/
function unregisterFrom ( event ){
var keep = [];
for (var i = 0; i< this.lastevent ; i++){
var ev = this[ i ];
if ( ev == event ){
ev.removeDelegate( this );
} else {
keep.push( ev );
}
this[ i ] = null;
}
//now fix it
this.lastevent = 0;
var l = keep.length;
for ( var i = 0; i < l; i++ ){
this[ this.lastevent++ ] = keep[ i ];
}
}
/**
* Disables the delegate until enable method is called.
*/
function disable (){
this.enabled = false;
}
/**
* Enables a delegate that has been disabled
*/
function enable (){
this.enabled = true;
}
/**
* Queue for delegates that are deferred during node initialization
* @access private
*/
static var __LZdelegatesQueue = [];
static var __restoreEnv = {};
static var __saveEnv = {};
/**
* Drain the delegates queue back to position POS
* @access private
*/
static function __LZdrainDelegatesQueue (pos) {
var evq = this.__LZdelegatesQueue;
var n = evq.length;
var i = pos;
if (i < n) {
var calledDelegates = new Array;
var lockedEvents = new Array;
while (i < n) {
var e = evq[i];
if (e === LzDelegate.__saveEnv) {
++i;
continue;
} else if (e === LzDelegate.__restoreEnv) {
var d;
while (d = calledDelegates.pop()) {
d.event_called = false;
}
var e;
while (e = lockedEvents.pop()) {
e.locked = false;
}
++i;
continue;
}
var d = evq[i+1];
var sd = evq[i+2];
// Mimic sendEvent which prohibits events and delegates from
// recursing
if (!e.locked) {
e.locked = true;
lockedEvents.push(e);
}
if (!d.event_called) {
d.event_called = true; //this delegate has been called
calledDelegates.push( d );
// d.execute( sd ); inlined
if (d.c[d.f]) d.c[d.f]( sd );
}
i+=3;
}
while (d = calledDelegates.pop()) {
d.event_called = false;
}
while (e = lockedEvents.pop()) {
e.locked = false;
}
}
evq.length = pos;
}
/** @access private */
function toString (){
return ("Delegate for " + this.c + " calls " + this.f + " " +
this.__delegateID );
}
if ($debug) {
/**
* If debugging, add an informative name
* @access private
*/
function _dbg_name (){
var name = Debug.formatToString("%0.48w.%s()", this.c, this.f);
if (this[0]) {
name += Debug.formatToString("/* handles %w%s */", this[0],
this[1]?" ...":"");
}
return name;
}
}
} // End of LzDelegate
/**
* <p>Events underly most of the functionality in OpenLaszlo
applications.
* Unlike events in similar systems, OpenLaszlo's events are point-
to-point, meaning that there is no general broadcast mechanism for
events, and events do not trickle up or down the instance hierarchy.
Instead, objects called <xref linkend="LzDelegate"/> <xref
linkend="LzDelegate.prototype.register"/> for events, and if they
try to register for an event that doesn't exist yet, the system
creates the event. </p>
*
* <p>You can create a delegate explicitly using the <classname
link="true">LzDelegate</classname> class, or implicitly by creating
an handler.</p>
* <p>
* Because of the loose type requirements in LZX, calling an event
that no delegate is listening for (and which therefore hasn't been
created) has no effect. This allows objects to publish many more
events than they actually need to create at runtime.
* </p>
*
* <p>There are two syntaxes with which you can specify an event
handler: in the tag used to create that object, or by using the
* <tagname link="true">handler</tagname> tag.</p>
*
* <p>To specify an event handler in an object-creation tag, simply
include it like any other attribute. For example,</p>
*
* <example class="code" extract="false">
* <view onmouseover="doSomething()">
* <method name="doSomething">
* // code to be executed when mouse is over the view
* </method>
* </view>
* </example>
*
* <p>If you use the <tagname link="true">handler</tagname> tag, you
do not need to include the handler in the tag that creates the
object.</p>
*
* <example class="code" extract="false">
* <view>
* <handler name="onmouseover">
* // code to be executed when the mouse is over the view
* </name>
* </view>
* </example>
*
* <p> The above two examples are functionally equivalent. Using
the <tagname>handler</tagname> tag, however, can often lead to more
readable code because it removes clutter from the object creation
tag.</p>
*
*
* <p>Use the <tagname link="true">event</tagname> tag to create the
events; then use the <method>sendEvent</method> method to dispatch
it. The following example illustrates how to create custom events.</p>
*
* <example class="program"
* title="A simple example of publishing and listening for
a custom event">
* <canvas height="40">
* <simplelayout/>
* <button name="eventSender"
* <em>onmouseover="this.customevent.sendEvent()"
* onmouseout="this.customevent.sendEvent()"/></em>
*
* <event name="customevent"/>
* <view bgcolor="red" width="20" height="20"
oninit="this.setupDelegate()">
* <method name="setupDelegate">
* this.del = new LzDelegate( this, "respondToEvent" );
* <em>this.del.register( eventSender , "customevent" );</em>
* </method>
* <method name="respondToEvent">
*
* this.setAttribute('x', this.x + 10);
* </method>
* </view>
* </canvas>
* </example>
*
* <p>
* Events can be sent with a single argument, which usually conveys
information about the property that changed. The default behavior of
the <xref linkend="LzNode.prototype.setAttribute"/> method is to set
the named property and send the event called "on" + property. This
is general mechanism that updates constraints in a OpenLaszlo
programs. For instance, when a view changes its <attribute>x</
attribute> position, it sends the event <event>onx</event> with the
new value for its <attribute>x</attribute> property.
*
* </p>
*
* <example class="program"
* title="Event sending in response to setting an attribute">
* <canvas height="40">
* <simplelayout/>
* <button name="eventSender"
* <em>onmouseover="this.setAttribute('avalue',
this.avalue + 10)"
* onmouseout="this.setAttribute('avalue', this.avalue +
5)"</em>>
* <attribute name="avalue" value="0"/>
* </button>
*
* <view bgcolor="red" width="20" height="20"
oninit="this.setupDelegate()">
* <method name="setupDelegate">
* this.del = new LzDelegate(this, "respondToEvent");
* this.del.register(eventSender, <em>"onavalue"</em>);
* </method>
* <method name="respondToEvent" args="v">
* this.setAttribute('x' , v);
* </method>
*
* </view>
* </canvas>
* </example>
*
* @shortdesc The sender in Laszlo's point-to-point event system.
* @see LzDelegate
*/
class LzEvent {
/**
* @param Object eventSender: The owner of this event
* @param String eventName: The name of this event.
*/
function initialize ( eventSender , eventName , d ){
super.initialize.apply(this, arguments);
var _evs = eventSender._events;
if (_evs == null ){
eventSender._events = [ this ];
} else {
_evs.push ( this );
}
eventSender[ eventName ] = this;
if ( d ){
this.delegateList = [d];
this.ready = true;
}else{
this.delegateList = [];
}
// If debugging, add an informative name
if ($debug) {
this._dbg_eventSender = eventSender;
this._dbg_eventName = eventName;
}
if ($profile) {
this._dbg_profileName = eventName;
}
}
// Because this is not created with Class
//prototype.classname = "LzEvent";
/** True when event is being sent.
* @type Boolean
*/
var locked = false;
/** True when event is ready to be sent.
*
*/
var ready = false;
/**
* Adds the given delegate to the event's delegate list. Although
this listed
* as a public function it should rarely be called explicitly -- it
is used
* exclusively by <b><i>LzDelegate</i>.register</b>
*
* @param LzDelegate d: The delegate to add to the list of
delegates called by the event.
*/
function addDelegate (d){
this.ready = true;
this.delegateList.push(d);
}
/**
* Sends the event, passing its argument as the data to all the
called
* delegates
*
* @param sd: The data to send with the event.
*/
function sendEvent ( sd ){
if ( this.locked ) { return; } //don't allow for multiple calls
var dll = this.delegateList.length;
if (dll == 0) { return; }
if ($profile) {
var nm = this._dbg_profileName;
if (nm) {
Profiler.event(nm, 'calls');
}
}
this.locked = true;
var calledDelegates = new Array;
var d;
var evq = LzDelegate.__LZdelegatesQueue;
var evqFlag = false;
for (var i = dll; i >= 0; i--){
d = this.delegateList[ i ];
//pointer may be bad due to deletions
if ( d && ! d.event_called){
d.event_called = true; //this delegate has been called
calledDelegates.push( d );
// We don't worry about deleted contexts here, because
// we assume that delegates registered on events are
// properly managed
if (d.enabled && d.c) {
if (d.c.__LZdeferDelegates) {
if (!evqFlag) {
evqFlag = true;
evq.push(LzDelegate.__saveEnv);
}
evq.push(this, d, sd);
} else {
// d.execute( sd ); inlined
if (d.c[d.f]) d.c[d.f]( sd );
}
}
}
}
if (evqFlag) {
evq.push(LzDelegate.__restoreEnv);
}
while (d = calledDelegates.pop() ){
d.event_called = false;
}
if ($profile) {
var nm = this._dbg_profileName;
if (nm) {
Profiler.event(nm, 'returns');
}
}
this.locked = false;
}
/**
* Removes the delegate from the delegate list. In practice, this
is rarely
* called explicitly, since it does not update the delegate's list
of stored
* events. Right now, this is called only by <b><i>LzDelegate</i>.
* unregisterAll</b> Delegates should support a simple unregister
command, that
* unregisters them for a single event, but to date, that has not
proven
* necessary
*
* @param LzDelegate d: The delegate to remove from the delegateList.
*/
function removeDelegate ( d ){
var dll = this.delegateList.length;
for (var i = 0; i < dll; i++){
if (this.delegateList[i] == d){
this.delegateList.splice(i, 1);
break;
}
}
if ( this.delegateList.length == 0 ){
this.ready = false;
}
}
/**
* Removes all delegates from call list
*/
function clearDelegates (){
while (this.delegateList.length ){
this.delegateList[ 0 ].unregisterFrom( this );
}
//this.delegateList = [];
this.ready = false;
}
/**
* Returns the number of delegates registered for the event
* @return Number: The number of delegates registered for the event.
*/
function getDelegateCount ( ){
return this.delegateList.length;
}
function toString (){
return ( "LzEvent");
}
if ($debug) {
/**
* If debugging, add an informative name
* @access private
*/
function _dbg_name (){
return Debug.formatToString("%0.48w.%s", this._dbg_eventSender,
this._dbg_eventName);
}
}
} // End of LzEvent