I am going to checkpoint the solution so far, which covers several of your test cases, but not this super one. I need to get some other stuff done, but don't want to lose this thread. Can you file a new Jira item with this?

On 2007-12-04, at 15:06 EST, André Bargull wrote:

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">
 * &lt;view onmouseover="doSomething()"&gt;
 *   &lt;method name="doSomething"&gt;
 *     // code to be executed when mouse is over the view
 *   &lt;/method&gt;
 * &lt;/view&gt;
 * </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">
 * &lt;view&gt;
 *  &lt;handler name="onmouseover"&gt;
 *    // code to be executed when the mouse is over the view
 *  &lt;/name&gt;
 * &lt;/view&gt;
 * </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">
 * &lt;canvas height="40"&gt;
 *   &lt;simplelayout/&gt;
 *   &lt;button name="eventSender"
 *           <em>onmouseover="this.customevent.sendEvent()"
 *           onmouseout="this.customevent.sendEvent()"/&gt;</em>
 *
 *   &lt;event name="customevent"/&gt;
* &lt;view bgcolor="red" width="20" height="20" oninit="this.setupDelegate()"&gt;
 *     &lt;method name="setupDelegate"&gt;
 *       this.del = new LzDelegate( this, "respondToEvent" );
 *       <em>this.del.register( eventSender , "customevent" );</em>
 *     &lt;/method&gt;
 *     &lt;method name="respondToEvent"&gt;
 *
 *       this.setAttribute('x', this.x + 10);
 *     &lt;/method&gt;
 *   &lt;/view&gt;
 * &lt;/canvas&gt;
 * </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">
 * &lt;canvas height="40"&gt;
 *   &lt;simplelayout/&gt;
 *   &lt;button name="eventSender"
* <em>onmouseover="this.setAttribute('avalue', this.avalue + 10)" * onmouseout="this.setAttribute('avalue', this.avalue + 5)"</em>&gt;
 *     &lt;attribute name="avalue" value="0"/&gt;
 *   &lt;/button&gt;
 *
* &lt;view bgcolor="red" width="20" height="20" oninit="this.setupDelegate()"&gt;
 *     &lt;method name="setupDelegate"&gt;
 *       this.del = new LzDelegate(this, "respondToEvent");
 *       this.del.register(eventSender, <em>"onavalue"</em>);
 *     &lt;/method&gt;
 *     &lt;method name="respondToEvent" args="v"&gt;
 *       this.setAttribute('x' , v);
 *     &lt;/method&gt;
 *
 *   &lt;/view&gt;
 * &lt;/canvas&gt;
 * </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


Reply via email to