Side Effects, what is this spell?

Let’s delve into the heart of ABAP’s RAP model, where these mysterious side effects play an essential role. Designed to optimize the user experience in Fiori applications in Draft mode, they enable intelligent synchronization of the graphical interface without overloading the backend.

1. By the way, what is a “side effect” in RAP?

Side effects are designed to refresh the UI in the case of a RAP BO built with DRAFT. Indeed :

  • In the case of RAP objects in Draft mode, the “stateless communication pattern” is used. This means that the UI does not reload all the object’s fields once one of them has been modified.
  • The big advantage is more efficient request operation, because otherwise there would be too many requests, slowdowns and network overloads, if everything had to be reloaded each time.
  • For example, if one of the RAP object’s fields is modified by the user, a PATCH request will be made to the RAP BO, and if the request is successful, there will be a 204 No content return (classic for a PATCH request). However, let’s imagine that the modification of this field also triggers a determination on another field. As there is no READ performed immediately afterwards, the UI is not updated with the new value.
  • The solution is the side effect. In our example, the side effect specifies that when this field is modified, the other field must also be reloaded.
  • When a side effect is created, the metadatas are modified to indicate to the UI that a side effect exists and that certain fields need to be recalculated when certain modifications are made.

To try to summarize the documentation, it is possible to create side effects for different cases:

  • Field: When a specific field is modified, it triggers the side effect (example explained above).
    side effects { field MyField affects Targets }
  • Action: When a user action is executed (e.g. Calculate, CheckAvailability), this triggers the side effect, so the UI is reloaded in this case.
    side effects { action MyAction affects Targets }
  • determine action: When a RAP determination is triggered, it can also cause a side effect.
    side effects { determine action MyDetermineAction executed on Sources affects Targets }
  • self: When the entity itself (any field) is modified, it may trigger reloads of other entities, but not itself.
    side effects { $self affects Targets }
  • event: When an event is raised on the backend, it can trigger a side effect. We’ll come back to this later with a concrete example.
    side effects { event MyEvent affects Targets }

In fact, in a side effect block, a target designates what you want to reload on the UI side when the side effect is triggered.

  • field: fields to be reloaded
  • permissions : Used to recalculate permissions
  • $self: Reloads the entire entity itself (not just a field).
  • entity: Reloads a complete associated entity via a CDS association
  • messages : Reload error / warning / info messages

Now that the aims of side effects are clear, let’s move on to our case studies.

2. Case studies

All the code is available on our Gitlab.

For our case, we’ll create a RAP object based on this table:

@EndUserText.label : 'test'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #RESTRICTED
define table zcrt_test {

  key client         : abap.clnt not null;
  key id             : sysuuid_x16 not null;
  text               : abap.char(50);
  text2              : abap.char(50);
  text3              : abap.char(50);
  status             : zstatus_test;
  criticality        : abap.int1;
  local_last_changed : abp_locinst_lastchange_tstmpl;
  createdby          : abp_creation_user;
  createdat          : abp_creation_tstmpl;
  changeby           : abp_lastchange_user;
  changeat           : abp_lastchange_tstmpl;

}

The purpose of this table is to store 3 text fields and a status whose value we’ll base on the various text fields => via a determination in the RAP object.

Then we use the ABAP RAP Generator Objects to generate a Draft-Enabled RAP object.

The rules we’re going to implement:

  • When the text field is filled, it must be impossible to fill the text2 field.
  • When the text field is filled, its value must be replicated in the text3 field.
  • When the text2 field is filled, it should be impossible to fill the text field.
  • Finally, when the object is saved, we want it to be processed via bgPF to determine its status. Once this asynchronous processing has been carried out, the UI must be set.

This video shows the expected result.

PART 1: Managing text, text2 and text3 fields

  • To manage the fact that text and text2 fields are not filled at the same time, simply implement the get_instance_features method and deactivate the field that is not to be filled:
METHOD get_instance_features.

    READ ENTITIES OF zr_crt_test IN LOCAL MODE
    ENTITY zrcrttest
    ALL FIELDS
    WITH CORRESPONDING #( keys )
    RESULT DATA(result_read).

    LOOP AT keys INTO DATA(key).
      DATA(instance) = result_read[ id = key-id ].
      IF instance-Text2 IS NOT INITIAL.
        APPEND VALUE #(
        %is_draft = key-%is_draft
        id = key-Id
        %field-text = if_abap_behv=>fc-f-read_only
        ) TO result.
      ENDIF.
      IF instance-Text IS NOT INITIAL.
        APPEND VALUE #(
        %is_draft = key-%is_draft
        id = key-Id
        %field-text2 = if_abap_behv=>fc-f-read_only
        ) TO result.
      ENDIF.
    ENDLOOP.


  ENDMETHOD.

Here, if the text field is filled, then the text2 field is set to read only via the if_abap_behv=>fc-f-read_only constant assigned to %field-text2 in the RESULT parameter.

Ditto in the other case.

  • Next, to ensure that text3 is identical to the text field, we create a determination that will be executed when the text field is modified
  METHOD text3.

    READ ENTITIES OF zr_crt_test IN LOCAL MODE
  ENTITY zrcrttest
  ALL FIELDS
  WITH CORRESPONDING #( keys )
  RESULT DATA(results_read).

    MODIFY ENTITIES OF zr_crt_test IN LOCAL MODE
ENTITY zrcrttest
UPDATE FIELDS ( text3 )
    WITH VALUE #(
        FOR update IN results_read
        ( %tky   = update-%tky
          text3 = update-text
          %control-text3 = if_abap_behv=>mk-on
) ).
  ENDMETHOD.
  • In the end, the behavior definition looks like this for now
managed implementation in class ZBP_R_CRT_TEST unique;
strict ( 2 );
with draft;
define behavior for ZR_CRT_TEST alias ZrCrtTest
persistent table zcrt_test

draft table ZCRT_TEST_D
etag master LocalLastChanged
authorization master ( global )
lock master total etag ChangeAt
{

  field ( readonly, numbering:managed )
  Id;

  field ( readonly )
  LocalLastChanged,
  CreatedBy,
  CreatedAt,
  ChangeBy,
  ChangeAt,
  Text3,
  Status;

  field ( features : instance )
  Text,
  Text2;

  create;
  update;
  delete;
  determination text3 on modify { field text; }

    draft action Activate optimized;
    draft action Discard;
    draft action Edit;
    draft action Resume;
    draft determine action Prepare;

  mapping for zcrt_test corresponding
    {
      Id               = id;
      Text             = text;
      Text2            = text2;
      Text3            = text3;
      LocalLastChanged = local_last_changed;
      CreatedBy        = createdby;
      CreatedAt        = createdat;
      ChangeBy         = changeby;
      ChangeAt         = changeat;
      Status           = status;
      Criticality      = criticality;
    }

}

However, we can see that there is a problem:

In fact, in the video, we can see that the fields are not updated.

When we look at the http calls, we see that when text fields are modified, a PATCH is successfully sent

with response 204 – No Content ( Normal if successful with a PATCH )

The problem here is that the Fiori application has no way of knowing that the characteristics of the text2 and text3 fields have also been modified.

Now, by adding side effects to the behavior definition: it works ( Cf. first video )

Now here’s the behavior definition:

managed implementation in class ZBP_R_CRT_TEST unique;
strict ( 2 );
with draft;
define behavior for ZR_CRT_TEST alias ZrCrtTest
persistent table zcrt_test

draft table ZCRT_TEST_D
etag master LocalLastChanged
authorization master ( global )
lock master total etag ChangeAt
{

  field ( readonly, numbering:managed )
  Id;

  field ( readonly )
  LocalLastChanged,
  CreatedBy,
  CreatedAt,
  ChangeBy,
  ChangeAt,
  Text3,
  Status;

  side effects
  {
    field text2 affects $self;
    field text affects $self;
  }

  field ( features : instance )
  Text,
  Text2;

  create;
  update;
  delete;
  determination text3 on modify { field text; }

    draft action Activate optimized;
    draft action Discard;
    draft action Edit;
    draft action Resume;
    draft determine action Prepare;

  mapping for zcrt_test corresponding
    {
      Id               = id;
      Text             = text;
      Text2            = text2;
      Text3            = text3;
      LocalLastChanged = local_last_changed;
      CreatedBy        = createdby;
      CreatedAt        = createdat;
      ChangeBy         = changeby;
      ChangeAt         = changeat;
      Status           = status;
      Criticality      = criticality;
    }

}

Here, by adding field text2 affects $self; and field text affects $self; I tell the UI to reload the entire entity when the text or text2 fields are modified. This reloads the feature controls (and therefore grays out the desired fields) as well as updating the text3 field, since all fields are reloaded.

The PATCH is still sent, but this time the feature controls and fields are reloaded via a GET performed immediately afterwards, as the Fiori application knows from the metadatas that this modification requires the entire entity to be reloaded.

PART 2: UI update for background status field calculation via bgPF

Our aim here is to demonstrate that it is possible to update the UI, even after the object has been saved and completed, in the event of asynchronous background modifications. To do this, we’re going to use Event-Driven Side Effects.

These event-driven Side Effects allow you to update the UI during asynchronous processes.

In concrete terms, we’re going to add an asynchronous modification via bgPF when the object is saved. This will modify the value of the status field according to the text values ( If at least one text field is filled in, the status is set to “Validated”, otherwise to “No Validation”. During the entire processing time, the field will be left in “Waiting for validation” to show the user that a calculation is in progress). In the case of this example, and to show that this is useful in the case of long processing times, we’ll add a WAIT of 4 seconds (as it’s in bgPF, the synchronous modification is finished and the user can take over even if the asynchronous processing is still in progress).

  • First, we create an action to be called by the bgPF via EML: action calculateStatus result [1] $self;
  METHOD calculateStatus.
    DATA : status_update TYPE TABLE FOR UPDATE zr_crt_test.

    READ ENTITIES OF zr_crt_test IN LOCAL MODE
  ENTITY zrcrttest
  ALL FIELDS
  WITH CORRESPONDING #( keys )
  RESULT DATA(results_read).

    MODIFY ENTITIES OF zr_crt_test IN LOCAL MODE
  ENTITY zrcrttest
  UPDATE FIELDS ( status )
      WITH VALUE #(
          FOR update IN results_read
          ( %tky   = update-%tky
            status = COND #( WHEN update-Text IS INITIAL AND update-Text2 IS INITIAL THEN 'N'
            ELSE 'V' )
            %control-Status = if_abap_behv=>mk-on
  ) ).

    READ ENTITIES OF zr_crt_test IN LOCAL MODE
  ENTITY zrcrttest
  ALL FIELDS
  WITH CORRESPONDING #( keys )
  RESULT results_read.

    result = VALUE #( FOR result_read IN results_read ( %tky = result_read-%tky
                                              %param    = result_read ) ).

  ENDMETHOD.
  • Then we create the class for the bgPF :
CLASS zbgpf_UI DEFINITION
  PUBLIC
  FINAL
  CREATE PUBLIC .

  PUBLIC SECTION.
    INTERFACES if_bgmc_op_single_tx_uncontr.

    METHODS constructor
      IMPORTING i_rap_bo_entity_key TYPE sysuuid_x16.

  PROTECTED SECTION.
  PRIVATE SECTION.
    DATA uuid TYPE sysuuid_x16.
ENDCLASS.



CLASS zbgpf_UI IMPLEMENTATION.

  METHOD constructor.
    uuid = i_rap_bo_entity_key.
  ENDMETHOD.

  METHOD if_bgmc_op_single_tx_uncontr~execute.

    WAIT UP TO 4 SECONDS.

    DATA : status_update TYPE TABLE FOR UPDATE zr_crt_test.

    READ ENTITIES OF zr_crt_test
             ENTITY zrcrttest
             ALL FIELDS
             WITH VALUE #( ( %is_draft = if_abap_behv=>mk-off
                             %key-Id = uuid
                            )  )
             RESULT DATA(entities)
             FAILED DATA(failed).

    IF entities IS NOT INITIAL.

      LOOP AT entities INTO DATA(entity).

        MODIFY ENTITIES OF zr_crt_test
        ENTITY zrcrttest
        EXECUTE calculateStatus FROM VALUE #( (  %key-Id = uuid ) ).

      ENDLOOP.
    ENDIF.

    COMMIT ENTITIES.
  ENDMETHOD.

ENDCLASS.
  • Finally, in behavior definition, we add with additional save and, in the save_modified method, we create the bgPF using the zbgpf_UI class, which modifies the status field.
  • We perform this action: RAISE ENTITY EVENT zr_crt_test~statusUpdated to inform via an event that the status field has been modified and the UI knows to reload it
managed implementation in class ZBP_R_CRT_TEST unique;
strict ( 2 );
with draft;
define behavior for ZR_CRT_TEST alias ZrCrtTest
persistent table zcrt_test
with additional save

draft table ZCRT_TEST_D
etag master LocalLastChanged
authorization master ( global )
lock master total etag ChangeAt
{

  field ( readonly, numbering:managed )
  Id;

  field ( readonly )
  LocalLastChanged,
  CreatedBy,
  CreatedAt,
  ChangeBy,
  ChangeAt,
  Text3,
  Status;

  event statusUpdated for side effects;

  side effects
  {
    field text2 affects $self;
    field text affects $self;
    event statusUpdated affects field ( Status, Criticality );
  }

  field ( features : instance )
  Text,
  Text2;

  create;
  update;
  delete;
  determination text3 on modify { field text; }
  determination criticality on modify { field Status; }
  determination status on modify { create; }
  determination statusOnSave on save { create; }
  action calculateStatusBGPF result [1] $self;
  action calculateStatus result [1] $self;
  validation validation on save { create; update; }

    draft action Activate optimized;
    draft action Discard;
    draft action Edit;
    draft action Resume;
    draft determine action Prepare
    {
        validation validation;
        determination statusOnSave;
    }

  mapping for zcrt_test corresponding
    {
      Id               = id;
      Text             = text;
      Text2            = text2;
      Text3            = text3;
      LocalLastChanged = local_last_changed;
      CreatedBy        = createdby;
      CreatedAt        = createdat;
      ChangeBy         = changeby;
      ChangeAt         = changeat;
      Status           = status;
      Criticality      = criticality;
    }

}
  METHOD save_modified.
    DATA : events_to_be_raised TYPE TABLE FOR EVENT zr_crt_test~statusUpdated.

    IF update-zrcrttest IS NOT INITIAL.
      LOOP AT update-zrcrttest ASSIGNING FIELD-SYMBOL(<update_zrcrttest>).

        "check if a process via bgpf shall be started
        IF     <update_zrcrttest>-Status          = 'W'
           AND <update_zrcrttest>-%control-Status = if_abap_behv=>mk-on.

          TRY.
              DATA(bgpf_status) = NEW zbgpf_UI( i_rap_bo_entity_key = <update_zrcrttest>-%key-id ).
              DATA background_process TYPE REF TO if_bgmc_process_single_op.
              background_process = cl_bgmc_process_factory=>get_default(  )->create(  ).
              background_process->set_operation_tx_uncontrolled( bgpf_status ).
              background_process->save_for_execution(  ).
            CATCH cx_bgmc INTO DATA(exception).
          ENDTRY.
          DATA(bgpf_update) = abap_true.
        ENDIF.

        IF   ( <update_zrcrttest>-Status = 'V' OR <update_zrcrttest>-Status = 'N' )
           AND <update_zrcrttest>-%control-Status = if_abap_behv=>mk-on.

          CLEAR events_to_be_raised.
          APPEND INITIAL LINE TO events_to_be_raised.
          events_to_be_raised[ 1 ] = CORRESPONDING #( <update_zrcrttest> ).

          RAISE ENTITY EVENT zr_crt_test~statusUpdated FROM events_to_be_raised.
        ENDIF.
      ENDLOOP.
    ENDIF.

    IF create-zrcrttest IS NOT INITIAL.
      LOOP AT create-zrcrttest ASSIGNING FIELD-SYMBOL(<create_zrcrttest>).

        "check if a process via bgpf shall be started
        IF     <create_zrcrttest>-Status          = 'W'
           AND <create_zrcrttest>-%control-Status = if_abap_behv=>mk-on.

          TRY.
              bgpf_status = NEW zbgpf_UI( i_rap_bo_entity_key = <create_zrcrttest>-%key-id ).
              background_process = cl_bgmc_process_factory=>get_default(  )->create(  ).
              background_process->set_operation_tx_uncontrolled( bgpf_status ).
              background_process->save_for_execution(  ).
            CATCH cx_bgmc INTO exception.
          ENDTRY.

        ENDIF.

      ENDLOOP.
    ENDIF.

  ENDMETHOD.

As the side effect is present in the behavior definition: event statusUpdated affects field ( Status, Criticality ); the Fiori IU creates a listener on the statusUpdated event, and when it is emitted following the modification made via bgPF, the IU performs a new GET to the BO RAP to retrieve the updated information ( here, Status and Criticality ).

Conclusion

You now know how to update the UI of your Fiori app using Draft. You’ve also seen that background processing doesn’t prevent you from updating your UI once it’s completed.

Finally, it’s important to note that this event-driven side effect also allows you to update the UI of all users connected to the same object. Indeed, as a listener is created by the Fiori application on this event, any UI currently displaying the modified object will be notified and updated!