← Back to tutorials

RAP from scratch · Lesson 6 of 6

Actions and determinations in RAP: Fiori buttons and automatic values on save

Add Lend and Return buttons that change a book's status, plus a determination that fills in values when a book is created, all in RAP with no JavaScript.

También disponible en español →

So far the library app shows books, creates them, edits them and validates them. What is missing is what a real library does: lend a book and take it back. In this lesson we add two buttons, Lend and Return, that change the status of a book. We also add an automatic rule: when a book is created, the system sets its status to “available” and its creation date by itself, without the user typing anything.

Everything lives in the RAP behavior layer plus one annotation. There is no JavaScript or UI5. You need to have completed lesson 5.

Prerequisites

  • The library from lessons 1 to 5 active on your system: tables zlib_author and zlib_book, the view ZR_LibBook, the projection ZC_LibBook with its Metadata Extension, the Service Binding ZUI_LIB_BOOK_O4, and the Behavior Definition with the class zbp_r_libbook from lesson 4.
  • A system with ABAP Cloud (BTP ABAP Environment or S/4HANA with ABAP Cloud) and ADT in Eclipse. [VERIFY: exact system version used to test the tutorial]

Guide code: the code follows ABAP Cloud syntax, but it was not compiled on an SAP system for this tutorial. If something does not activate on your system, the ADT message points to the exact line.

Action and determination: the difference

Both are business logic you write in ABAP, but they are triggered differently:

Action Determination
Who triggers it The user, by pressing a button The framework, when something changes
Example in the library Lend, Return Set “available” when a book is created
Declared with action determination … on modify or on save
Shown in the UI Yes, with a button annotation No: the user never sees it
User presses "Lend" ──► action lendBook ──► changes Status to "lent"

User creates a book ──► framework detects "create" ──► determination
                                                        setInitialValues
                                                        (Status and creation date)

A practical rule: if the logic must happen because the user decided so, it is an action. If it must happen whenever something changes, without anyone asking, it is a determination. A validation (lesson 4) checks the data; a determination completes or calculates it.

Step 1: the book status in the model

Why: a lent book and an available one are different, so we need a field to remember it. We also add created_on, the creation date that the determination of step 4 will fill in. (RAP already stores local_created_at, a timestamp it fills in by itself. created_on is our own field and exists only to practice with a determination.)

1.1 The table. Open zlib_book in ADT, replace its content with this and activate it with Ctrl+F3. Complete file; the only new parts are status and created_on:

@EndUserText.label : 'Library: books'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #A
@AbapCatalog.dataMaintenance : #RESTRICTED
define table zlib_book {
  key client            : abap.clnt not null;
  key book_uuid         : sysuuid_x16 not null;
  book_id               : abap.numc(8);
  title                 : abap.char(80);
  author_id             : abap.numc(6);
  pages                 : abap.int4;
  isbn                  : abap.char(17);
  rating                : abap.int1;
  @Semantics.amount.currencyCode : 'zlib_book.currency_code'
  price                 : abap.curr(11,2);
  currency_code         : abap.cuky;
  status                : abap.char(1);
  created_on            : abap.dats;
  local_created_by      : abp_creation_user;
  local_created_at      : abp_creation_tstmpl;
  local_last_changed_by : abp_locinst_lastchange_user;
  local_last_changed_at : abp_locinst_lastchange_tstmpl;
  last_changed_at       : abp_lastchange_tstmpl;
}

The status is one character: A = available, L = lent.

1.2 The test data. The four existing books have an empty status. Open zcl_lib_fill_data (lesson 2) and, inside the loop LOOP AT books ASSIGNING FIELD-SYMBOL(<book>), add these two lines right before TRY.. Excerpt of ZCL_LIB_FILL_DATA, method if_oo_adt_classrun~main; the rest of the class does not change:

    LOOP AT books ASSIGNING FIELD-SYMBOL(<book>).
      <book>-status     = 'A'.
      <book>-created_on = cl_abap_context_info=>get_system_date( ).
      TRY.

Activate it and run it with F9. It reloads the four books, now as available.

1.3 The Root View Entity. Open ZR_LibBook and add three fields between currency_code as CurrencyCode, and @Semantics.user.createdBy: true. Excerpt of ZR_LibBook (lesson 2); the rest of the view does not change:

      currency_code         as CurrencyCode,
      status                as Status,
      cast(
        case status
          when 'A' then 'Available'
          when 'L' then 'Lent'
          else ''
        end as abap.char( 12 ) ) as StatusText,
      created_on            as CreatedOn,

StatusText is a calculated field, like the traffic light of lesson 5. It exists because “Lent” tells the user more than an L. Activate the view.

1.4 The projection. A field that is not in the projection does not reach the service. ZC_LibBook complete (the one from lesson 5 with three new lines: Status, StatusText and CreatedOn):

@AccessControl.authorizationCheck: #NOT_REQUIRED
@Metadata.allowExtensions: true
@EndUserText.label: 'Library: books (app)'
@Search.searchable: true
define root view entity ZC_LibBook
  provider contract transactional_query
  as projection on ZR_LibBook
{
  key BookUuid,
      @Search.defaultSearchElement: true
      BookId,
      @Search.defaultSearchElement: true
      @Search.fuzzinessThreshold: 0.8
      Title,
      AuthorId,
      _Author.Name as AuthorName,
      Pages,
      Isbn,
      Rating,
      RatingCriticality,
      Price,
      CurrencyCode,
      Status,
      StatusText,
      CreatedOn,
      LocalLastChangedAt
}

Activate it.

Step 2: declare the actions in the Behavior Definition

Why: the Behavior Definition is the contract of the business object. Until an action is declared here, it does not exist for anyone: not for the class, not for the service, not for the button.

Open the Behavior Definition of ZR_LibBook (the one from lesson 4). Complete file, with this step’s changes: Status and CreatedOn read-only, the two actions, and the mapping of CreatedOn:

managed implementation in class zbp_r_libbook unique;
strict ( 2 );

define behavior for ZR_LibBook alias Book
persistent table zlib_book
lock master
authorization master ( instance )
etag master LocalLastChangedAt
{
  create;
  update;
  delete;

  field ( readonly, numbering : managed ) BookUuid;
  field ( readonly ) LocalCreatedBy, LocalCreatedAt, LocalLastChangedBy,
                     LocalLastChangedAt, LastChangedAt, Status, CreatedOn;
  field ( mandatory ) BookId, Title, AuthorId;

  validation validatePages  on save { create; field Pages; }
  validation validateRating on save { create; field Rating; }

  action lendBook   result [1] $self;
  action returnBook result [1] $self;

  mapping for zlib_book corresponding
  {
    BookUuid           = book_uuid;
    BookId             = book_id;
    AuthorId           = author_id;
    CurrencyCode       = currency_code;
    CreatedOn          = created_on;
    LocalCreatedBy     = local_created_by;
    LocalCreatedAt     = local_created_at;
    LocalLastChangedBy = local_last_changed_by;
    LocalLastChangedAt = local_last_changed_at;
    LastChangedAt      = last_changed_at;
  }
}

Three new things:

  • Status and CreatedOn as readonly: the user cannot type them from the app. Only the action and the determination change them, from inside the business object.
  • action lendBook result [1] $self;: an instance action, that is, it acts on one specific book. result [1] $self says it returns the modified book, so Fiori refreshes the screen with the new status.
  • No parameter: Lend and Return need no input from the user. If you wanted to ask, for example, for the member’s name, the action would take a parameter with an abstract entity: action lendBook parameter ZA_LibLend result [1] $self;. This is a sketch, not executable code: you would have to define the entity ZA_LibLend, and the library has no members table. We leave it out here.

When you activate the definition, ADT warns that the methods are missing in the class. That is what step 3 does.

The actions must also pass through the projection. Open the Behavior Definition of ZC_LibBook (lesson 4). Complete file:

projection;
strict ( 2 );

define behavior for ZC_LibBook alias Book
use etag
{
  use create;
  use update;
  use delete;

  use action lendBook;
  use action returnBook;
}

This is what exposing the action in the projection means: use action makes it reach the OData service. The Service Definition does not change, because it already exposes ZC_LibBook.

Step 3: implement the actions

Why: the declaration says the action exists. The implementation says what it does. An action follows the same pattern as the validation in lesson 4: read, check, reject with failed and reported, and write.

Open the class zbp_r_libbook, Local Types tab. First, the handler definition. This is the complete definition section of lhc_book: the constants and the two new actions are added, and the validations stay as they were:

CLASS lhc_book DEFINITION INHERITING FROM cl_abap_behavior_handler.
  PRIVATE SECTION.
    CONSTANTS:
      BEGIN OF book_status,
        available TYPE zlib_book-status VALUE 'A',
        lent      TYPE zlib_book-status VALUE 'L',
      END OF book_status.

    METHODS get_instance_authorizations FOR INSTANCE AUTHORIZATION
      IMPORTING keys REQUEST requested_authorizations FOR Book RESULT result.

    METHODS validatePages FOR VALIDATE ON SAVE
      IMPORTING keys FOR Book~validatePages.

    METHODS validateRating FOR VALIDATE ON SAVE
      IMPORTING keys FOR Book~validateRating.

    METHODS lendBook FOR MODIFY
      IMPORTING keys FOR ACTION Book~lendBook RESULT result.

    METHODS returnBook FOR MODIFY
      IMPORTING keys FOR ACTION Book~returnBook RESULT result.
ENDCLASS.

Then the two methods. Excerpt of lhc_book, IMPLEMENTATION section: paste them inside the implementation, below validateRating. The rest of the class does not change:

  METHOD lendBook.
    " 1. Read the books the button was pressed on
    READ ENTITIES OF ZR_LibBook IN LOCAL MODE
      ENTITY Book
        FIELDS ( Title Status )
        WITH CORRESPONDING #( keys )
      RESULT DATA(books).

    DATA to_update TYPE TABLE FOR UPDATE ZR_LibBook.

    LOOP AT books INTO DATA(book).
      IF book-Status = book_status-lent.
        " 2. Already lent: reject it and explain why
        APPEND VALUE #( %tky = book-%tky ) TO failed-book.
        APPEND VALUE #( %tky             = book-%tky
                        %action-lendBook = if_abap_behv=>mk-on
                        %msg             = new_message_with_text(
                                             severity = if_abap_behv_message=>severity-error
                                             text     = |The book "{ book-Title }" is already lent| ) )
          TO reported-book.
      ELSE.
        APPEND VALUE #( %tky = book-%tky Status = book_status-lent ) TO to_update.
      ENDIF.
    ENDLOOP.

    " 3. Write the new status of the books that can be lent
    MODIFY ENTITIES OF ZR_LibBook IN LOCAL MODE
      ENTITY Book
        UPDATE FIELDS ( Status )
        WITH to_update.

    " 4. Return the modified book, as result [1] $self promises
    READ ENTITIES OF ZR_LibBook IN LOCAL MODE
      ENTITY Book
        ALL FIELDS
        WITH CORRESPONDING #( to_update )
      RESULT DATA(updated).

    result = VALUE #( FOR row IN updated ( %tky = row-%tky %param = row ) ).
  ENDMETHOD.

  METHOD returnBook.
    READ ENTITIES OF ZR_LibBook IN LOCAL MODE
      ENTITY Book
        FIELDS ( Title Status )
        WITH CORRESPONDING #( keys )
      RESULT DATA(books).

    DATA to_update TYPE TABLE FOR UPDATE ZR_LibBook.

    LOOP AT books INTO DATA(book).
      IF book-Status <> book_status-lent.
        APPEND VALUE #( %tky = book-%tky ) TO failed-book.
        APPEND VALUE #( %tky               = book-%tky
                        %action-returnBook = if_abap_behv=>mk-on
                        %msg               = new_message_with_text(
                                               severity = if_abap_behv_message=>severity-error
                                               text     = |The book "{ book-Title }" is not lent| ) )
          TO reported-book.
      ELSE.
        APPEND VALUE #( %tky = book-%tky Status = book_status-available ) TO to_update.
      ENDIF.
    ENDLOOP.

    MODIFY ENTITIES OF ZR_LibBook IN LOCAL MODE
      ENTITY Book
        UPDATE FIELDS ( Status )
        WITH to_update.

    READ ENTITIES OF ZR_LibBook IN LOCAL MODE
      ENTITY Book
        ALL FIELDS
        WITH CORRESPONDING #( to_update )
      RESULT DATA(updated).

    result = VALUE #( FOR row IN updated ( %tky = row-%tky %param = row ) ).
  ENDMETHOD.

What each block does:

  1. READ ENTITIES … IN LOCAL MODE: as in the validation, the action receives only the keys (keys), so you have to read the data. IN LOCAL MODE skips the authorization and feature-control checks, which the framework already did before getting here.
  2. failed and reported: the same pair as in lesson 4. failed marks the book as rejected; reported carries the message. Here the message is tied to the action with %action-lendBook, not to a field.
  3. MODIFY ENTITIES … UPDATE FIELDS ( Status ): writes the new status. UPDATE FIELDS ( Status ) limits the change to that field. Note that Status is readonly toward the outside, but it can be written here because IN LOCAL MODE is the business object itself.
  4. The result: it reads the updated books again and returns them in the shape result [1] $self requires: the key (%tky) and the content (%param). Only the books in to_update are read, so the rejected ones are not returned.

Activate the class with Ctrl+F3.

Step 4: the determination

Why: when a book is created, the user should not have to type the initial status or the creation date. The system does it, always the same way, in one place.

4.1 Declare it. In the Behavior Definition of ZR_LibBook, add one line below the validations. Excerpt of the definition from step 2:

  validation validatePages  on save { create; field Pages; }
  validation validateRating on save { create; field Rating; }

  determination setInitialValues on modify { create; }

Read the line from right to left: when a book is created, run setInitialValues, right after the modification. The { create; } block is the trigger: without it, the determination never runs.

4.2 Implement it. In zbp_r_libbook, Local Types tab, add the declaration to lhc_book (excerpt, next to the other METHODS):

    METHODS setInitialValues FOR DETERMINE ON MODIFY
      IMPORTING keys FOR Book~setInitialValues.

And the implementation, inside lhc_book IMPLEMENTATION (excerpt; the rest does not change):

  METHOD setInitialValues.
    READ ENTITIES OF ZR_LibBook IN LOCAL MODE
      ENTITY Book
        FIELDS ( Status )
        WITH CORRESPONDING #( keys )
      RESULT DATA(books).

    " Respect the value if the book already has a status
    DELETE books WHERE Status IS NOT INITIAL.
    CHECK books IS NOT INITIAL.

    MODIFY ENTITIES OF ZR_LibBook IN LOCAL MODE
      ENTITY Book
        UPDATE FIELDS ( Status CreatedOn )
        WITH VALUE #( FOR book IN books
                      ( %tky      = book-%tky
                        Status    = book_status-available
                        CreatedOn = cl_abap_context_info=>get_system_date( ) ) ).
  ENDMETHOD.

Activate everything. cl_abap_context_info=>get_system_date( ) is the ABAP Cloud way to ask for today’s date.

on modify or on save?

The difference is when it runs:

  • on modify: right after each change, within the same request. The value already exists when the service answers, so the screen shows it immediately. This is what we need: initial values and calculations the user must see.
  • on save: at save time, right before writing to the database. It is for what must be final at that moment: assigning a number that cannot repeat, or completing data that depends on the final state. The user does not see the result until the save finishes.

Practical rule: if the value is a visible aid, on modify. If it is a guarantee that must hold at save, on save.

Step 5: the button in Fiori Elements

Why: the action already exists in the service, but the UI does not know it must draw a button. You tell it with a @UI annotation in the Metadata Extension, not in JavaScript.

Where: in ADT, in the Project Explorer, open your package → Core Data Services → Metadata Extensions → the extension of ZC_LibBook (the one from lesson 3, extended in lesson 5). [VERIFY: exact tree path in the ADT version used]

Excerpt of the Metadata Extension of ZC_LibBook. Add these blocks before @UI.hidden: true LocalLastChangedAt;; the rest does not change:

  @UI.hidden: true
  Status;

  @UI: {
    lineItem: [
      { position: 80, label: 'Status' },
      { type: #FOR_ACTION, dataAction: 'lendBook',   label: 'Lend',   position: 81 },
      { type: #FOR_ACTION, dataAction: 'returnBook', label: 'Return', position: 82 } ],
    identification: [
      { position: 80, label: 'Status' },
      { type: #FOR_ACTION, dataAction: 'lendBook',   label: 'Lend',   position: 81 },
      { type: #FOR_ACTION, dataAction: 'returnBook', label: 'Return', position: 82 } ] }
  StatusText;

  @UI: {
    lineItem:       [ { position: 90, label: 'Created on' } ],
    identification: [ { position: 90, label: 'Created on' } ] }
  CreatedOn;

Where each button appears:

  • type: #FOR_ACTION in lineItem: the button appears above the table on the list page. It acts on the rows the user selects.
  • type: #FOR_ACTION in identification: the button appears in the toolbar of the detail page, top right.
  • dataAction: 'lendBook': the name of the action, as it appears in the Behavior Definition of the projection.

The buttons are annotated on the StatusText field because an action annotation must hang from a visible field; Status is hidden with @UI.hidden. Activate the Metadata Extension. The Service Binding does not need to be published again; you do have to reload the preview.

Step 6: test in the preview

Open the Service Binding ZUI_LIB_BOOK_O4, select Book and press Preview. Press Go.

6.1 Lend. The list shows the columns Status and Created on, and every book is Available. [SCREENSHOT: list with the Status column]

  1. Open Aves del sur. Top right you see the Lend and Return buttons. [SCREENSHOT: detail page with both buttons]
  2. Press Lend. The page refreshes and the status becomes Lent. [SCREENSHOT: detail page with status Lent]

6.2 Lend twice. With the book already lent, press Lend again. The action is rejected and Fiori shows the error message The book “Aves del sur” is already lent, changing nothing. [SCREENSHOT: error message] This is the failed and reported from step 3. The message belongs to the action, not to a field: no field turns red, it appears in the message window.

6.3 Return. Press Return: the status goes back to Available. If you press it with the book already available, you get the message is not lent.

6.4 The determination. Go back to the list, press Create and fill in only what is mandatory (Book ID, Title, Author ID), with a valid rating and more than 0 pages. Do not touch the status or the date: they are not in the create form, because they are readonly. Save. In the list, the new book shows as Available, with today’s date. [SCREENSHOT: new book with status and date]

6.5 Check the table. In ADT open zlib_book and press F8. The lent book has L in status; the new book has A in status and today’s date in created_on.

Disable the button according to the status

As it stands, both buttons are always active and the server protects with the error message. It is better if the button cannot be pressed when it makes no sense. This is called instance feature control: RAP calculates, book by book, which actions are active.

In the Behavior Definition of ZR_LibBook, change the two action lines (excerpt):

  action ( features : instance ) lendBook   result [1] $self;
  action ( features : instance ) returnBook result [1] $self;

In lhc_book DEFINITION, add (excerpt):

    METHODS get_instance_features FOR INSTANCE FEATURES
      IMPORTING keys REQUEST requested_features FOR Book RESULT result.

And in lhc_book IMPLEMENTATION, the complete method (excerpt):

  METHOD get_instance_features.
    READ ENTITIES OF ZR_LibBook IN LOCAL MODE
      ENTITY Book
        FIELDS ( Status )
        WITH CORRESPONDING #( keys )
      RESULT DATA(books).

    result = VALUE #(
      FOR book IN books
      ( %tky               = book-%tky
        %action-lendBook   = COND #( WHEN book-Status = book_status-lent
                                     THEN if_abap_behv=>fc-o-disabled
                                     ELSE if_abap_behv=>fc-o-enabled )
        %action-returnBook = COND #( WHEN book-Status = book_status-lent
                                     THEN if_abap_behv=>fc-o-enabled
                                     ELSE if_abap_behv=>fc-o-disabled ) ) ).
  ENDMETHOD.

Activate and reload the preview. On an available book, Return is disabled; on a lent one, Lend is. [SCREENSHOT: detail page with a disabled button] The checks from step 3 stay: an OData client can call the action without going through the button, and the business rule cannot depend on the screen. With the buttons disabled you can no longer trigger the error from the UI; that is why 6.2 came first.

Common errors

Symptom Cause Fix
The buttons do not appear use action is missing in the projection’s Behavior Definition, or the #FOR_ACTION annotation is not in the Metadata Extension Add use action lendBook; and use action returnBook; in ZC_LibBook, and the #FOR_ACTION blocks from step 5
The button appears but pressing it gives an “unknown action” error dataAction does not match the action name, case included Copy the exact name from the Behavior Definition
The determination never runs and the new book has no status The line has no trigger (on modify { create; }), or the trigger does not cover the case Declare { create; }; to react to the change of a field, { field Pages; }
The error message does not show next to any field A message from an action is tied to the action (%action-…), not to a field That is expected. To mark a field, add %element-<Field> = if_abap_behv=>mk-on to reported; it only turns red if that field is on screen
ADT warns that methods are missing in the class after activating the definition The Behavior Definition declares actions or determinations the class does not implement Press Ctrl+1 on the warning to generate the methods, or paste the declaration from step 3
The status does not change on screen The preview caches the service metadata Reload the preview

Summary

  • An action is triggered by the user with a button. It is declared with action in the Behavior Definition, implemented with FOR MODIFY, and its result is declared with result [1] $self.
  • A determination is triggered by the framework according to its trigger (create, field …). With on modify the value shows up at once; with on save, it is fixed at save time.
  • Both read with READ ENTITIES … IN LOCAL MODE, write with MODIFY ENTITIES … IN LOCAL MODE and reject with failed and reported.
  • The button is drawn with #FOR_ACTION in the Metadata Extension, and enabled or disabled with instance features.

The library now has data, service, UI, rules and actions. In the next lesson of the series we continue with the behavior layer. [VERIFY: topic of rap-07 before naming it]

All tutorials are in the tutorial index.