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.
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_authorandzlib_book, the viewZR_LibBook, the projectionZC_LibBookwith its Metadata Extension, the Service BindingZUI_LIB_BOOK_O4, and the Behavior Definition with the classzbp_r_libbookfrom 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:
StatusandCreatedOnasreadonly: 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] $selfsays 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 entityZA_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:
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 MODEskips the authorization and feature-control checks, which the framework already did before getting here.failedandreported: the same pair as in lesson 4.failedmarks the book as rejected;reportedcarries the message. Here the message is tied to the action with%action-lendBook, not to a field.MODIFY ENTITIES … UPDATE FIELDS ( Status ): writes the new status.UPDATE FIELDS ( Status )limits the change to that field. Note thatStatusisreadonlytoward the outside, but it can be written here becauseIN LOCAL MODEis the business object itself.- The
result: it reads the updated books again and returns them in the shaperesult [1] $selfrequires: the key (%tky) and the content (%param). Only the books into_updateare 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_ACTIONinlineItem: the button appears above the table on the list page. It acts on the rows the user selects.type: #FOR_ACTIONinidentification: 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]
- Open Aves del sur. Top right you see the Lend and Return buttons. [SCREENSHOT: detail page with both buttons]
- 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
actionin the Behavior Definition, implemented withFOR MODIFY, and its result is declared withresult [1] $self. - A determination is triggered by the framework according to its trigger (
create,field …). Withon modifythe value shows up at once; withon save, it is fixed at save time. - Both read with
READ ENTITIES … IN LOCAL MODE, write withMODIFY ENTITIES … IN LOCAL MODEand reject withfailedandreported. - The button is drawn with
#FOR_ACTIONin 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.
Get new tutorials by email
One email when something new is published. No spam; unsubscribe anytime.
I will email you to confirm your subscription. Data handled under the privacy policy.