javascriptsapui5

How to properly attach and detach event handler in UI5


I have problems with data binding of my custom control.

My control inherits from sap.m.Input and extends it with a special value helper. One of my new properties of my new control is a simple header for the value help dialog. This is bound to an i18n model.

When I now use my control in a normal form, everything works. The title is bound correctly and shows the value of the bound i18n property in that model. If I use my control as a template in a column of a sap.ui.table control, it only shows the default value of the title property. Data binding does not seem to work. But is still working on the inherited properties (such as value).

For simplification here my control which now has only that title property and if value help is requested, it shows the current value in an alert box. In table, it shows the default value. And without table, it shows the bound value from i18n model.

Here the simplified control code:

sap.ui.define([
  "sap/ui/core/Control",
  "sap/m/Input",
], function(Control, Input) {
  "use strict";

  return Input.extend("DvpClsSuggestInput", {
    "metadata": {
        "properties": {
          // Title of Value-Help Dialog
          "vhTitle": {
            type: "string",
            defaultValue: "Title"
          }
        }
      },
    
      init: function() {
        Input.prototype.init.apply(this, arguments);
        this.setShowValueHelp(true);
        this.attachValueHelpRequest(this.onValueHelpRequest.bind(this));
      },
    
      onValueHelpRequest: function(oEvent) {
        var lvTitle = this.getVhTitle();
        alert(lvTitle);
      },

    });
  });
});

Usage in sap.ui.table.Table (which doesn't work and shows the default value of the title property):

<table:Column>
  <m:Label text="{i18gn>HausWaehrung}" />
  <table:template>
    <dvp:MyInput
      value="{ path: 'Inv>Hwaer', type: 'sap.ui.model.type.String' }"
      vhTitle="{i18n>Currency}" />
  </table:template>
</table:column>         

Usage which works:

<VBox>
  <dvp:MyInput
    value="{ path: 'Cls>/Currency', type: 'sap.ui.model.type.String' }"
    vhTitle="{i18n>Currency}" />
</VBox>

Once again, binding against the value property works in both ways. Problem only exists with my own property vhTitle. Any Ideas are welcome.


Solution

  • Do NOT use .bind when attaching event handlers to ManagedObject's events. The same applies to detaching event handlers. UI5 has its own documented mechanism for passing listener objects for those cases.

    Example 1

    Attaching / detaching a valueHelpRequest-handler using the corresponding APIs and passing values to the list of arguments as documented in the API reference:

    myInput.attachValueHelpRequest(/*obj?,*/this.onVHRequest, this); // No .bind!
    // ...
    myInput.detachValueHelpRequest(this.onVHRequest, this); // Same references

    Example 2

    Attaching an event handler on control instantiation as documented in ManagedObject's API reference (All controls are ManagedObjects):

    new MyInput({
      // attach
      valueHelpRequest: [/*obj?,*/this.onVHRequest, this]
    });
    // ...
    myInput.detachValueHelpRequest(this.onVHRequest, this);

    Valid Names and Value Ranges:

    • [...]
    • For events, either a function (event handler) is accepted or an array of length 2 where the first element is a function and the 2nd element is an object to invoke the method on; or an array of length 3, where the first element is an arbitrary payload object, the second one is a function and the 3rd one is an object to invoke the method on [...].

    Example 3

    If this in the event handler is supposed to be the object that provides the event, passing the listener object can be omitted when registering the handler. This is described in the API reference as well:

    If <oListener> is not specified, the handler function is called in the context of the event provider.

    Drawbacks of using Function.prototype.bind in UI5

    1. When calling .bind on a function, an entire new function is created!

      const myFn = function() {};
      myFn === myFn.bind(); // returns: false
      

      Meaning if a handler is passed with .bind, that handler becomes never detachable because detachEvent awaits the same function reference and the same listener object reference as when attachEvent was called.

    2. To make things worse, the function created with .bind won't let you change the previously passed thisArg (this) even if the EventProvider tries to call the function afterwards with a different thisArg. This limitation is described in the ECMAScript specification (See Note 2), and also the cause of the issue described in the question. When ManagedObject clones the template control for aggregation binding, the listener cannot be overwritten!