In this tutorial, we will build a Fiori Elements application to perform CRUD operations with the BOPF Framework using CDS annotations, without writing a single line of UI5 code.
The Fiori application that we will build today will be focused on visitor management. We will create a database table called “VISITOR” to store visitor information. The table will include fields such as Visit ID, First Name, Last Name, Company Name, Telephone, E-Mail, Reason, Host Information, Visit Date, Check-In Date, Check-Out Date, and Status.
There are a few fields in the table that will be created which contains fixed values so first step we need to do is creating data elements and domains for this fields.
Now, let’s create simple table to store visitor records.
VISITOR TABLE | KEY | Data Element | Domain | Fixed Value | |
---|---|---|---|---|---|
MANDT | X | MANDT | |||
GUID | GUID | X | GUID | ||
Visit ID | VISITID | VISITID | |||
First Name | FNAME | FNAME | |||
Last Name | LNAME | LNAME | |||
Company Name | CMPNY | CMPNY | |||
Telephone Country Code | TELCODE | TELCODE | |||
Telephone | TELNUM | TELNUM | |||
Reason | REASON | REASON | ZXXX_DO_VIS_REASON | Meeting, Interview, Maintance, Other | |
Host Information | VHOST | VHOST | |||
Visit Date | VDATE | VDATE | |||
Check-In Date | INTIME | INTIME | |||
Check-Out Date | OUTTIME | OUTTIME | |||
Status | STATUS | STATUS | ZXXX_DO_VIS_STATUS | Done, Postponed, Canceled, In Progress |
We need to create two CDS views which called Basic and Projection Views.
Basic View: interacts with database for fetching data
Projection View: created on top of basic views and contains Annotations
Using the steps shown below, create two CDS Views as follows:
@AbapCatalog.sqlViewName: 'ZXXXCDSVIS01'
@AbapCatalog.compiler.compareFilter: true
@AbapCatalog.preserveKey: true
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Visitor CDS'
define view ZXXX_CDS_VIS_01
as select from zxxx_visitor2
{
key guid as Guid,
visitid as Visitid,
fname as Fname,
lname as Lname,
cmpny as Cmpny,
telcode as Telcode,
telnum as Telnum,
email as Email,
reason as Reason,
vhost as Vhost,
vdate as Vdate,
intime as Intime,
outtime as Outtime,
status as Status
}
@AbapCatalog.sqlViewName: 'ZXXXCDSVIS02'
@AbapCatalog.compiler.compareFilter: true
@AbapCatalog.preserveKey: true
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Visitor CDS View'
@ObjectModel:{
modelCategory: #BUSINESS_OBJECT,
compositionRoot: true,
representativeKey: 'Guid',
semanticKey: ['Visitid'],
transactionalProcessingEnabled: true,
writeActivePersistence: 'ZXXX_VISITOR2',
// enable crud
createEnabled: true,
updateEnabled: true,
deleteEnabled: true
}
@UI.headerInfo: {typeName: 'Visitor', typeNamePlural: 'Visitors', title: {value: 'Visitid'}}
@UI: {
presentationVariant: [{
sortOrder: [{
by: 'Visitid',
direction: #ASC
}],
visualizations: [{
type: #AS_LINEITEM
}]
}]
}
@OData.publish: true
define view ZXXX_CDS_VIS_02
as select from ZXXX_CDS_VIS_01
association [0..1] to ZXXX_CDS_VIS_REASON_DOM as _ReasonValueHelp on $projection.Reason = _ReasonValueHelp.Low
association [0..1] to ZXXX_CDS_VIS_STATUS_DOM as _StatusValueHelp on $projection.Status = _StatusValueHelp.Low
{
@UI.facet: [
// Section General Information
{
label: 'General Information',
position: 10,
type: #COLLECTION,
id: 'GeneralInfo'
},
{
label: 'General',
purpose: #STANDARD,
position: 10,
type: #IDENTIFICATION_REFERENCE,
parentId: 'GeneralInfo'
}
]
@UI.hidden: true
key Guid,
@EndUserText.label: 'Visit ID'
@UI: {
lineItem: [{ position: 10}],
identification: [{position: 10}],
fieldGroup: [{qualifier: 'Basic'}]
}
@ObjectModel.mandatory: true
@ObjectModel.readOnly: true
Visitid,
@EndUserText.label: 'First Name'
@Consumption.valueHelpDefinition: [ {
entity: {
name: 'ZXXX_CDS_VIS_02',
element: 'Fname'
}
} ]
@UI: {
lineItem: [{ position: 20}],
identification: [{position: 20}],
fieldGroup: [{qualifier: 'Basic'}]
}
@ObjectModel.mandatory: true
Fname,
@EndUserText.label: 'Last Name'
@Consumption.valueHelpDefinition: [ {
entity: {
name: 'ZXXX_CDS_VIS_02',
element: 'Lname'
}
} ]
@UI: {
lineItem: [{ position: 30}],
identification: [{position: 30}],
fieldGroup: [{qualifier: 'Basic'}]
}
@UI.selectionField: [{ position: 11 }]
@ObjectModel.mandatory: true
Lname,
@EndUserText.label: 'Company'
@Consumption.valueHelpDefinition: [ {
entity: {
name: 'ZXXX_CDS_VIS_02',
element: 'Cmpny'
}
} ]
@UI: {
lineItem: [{ position: 40}],
identification: [{position: 40}],
fieldGroup: [{qualifier: 'Basic'}]
}
@UI.selectionField: [{ position: 12 }]
@ObjectModel.mandatory: true
Cmpny,
@EndUserText.label: 'Tel.No. Country Code'
@UI.hidden: true
@UI: {
lineItem: [{ position: 50}],
identification: [{position: 50}],
fieldGroup: [{qualifier: 'Basic'}]
}
@ObjectModel.mandatory: true
Telcode,
@EndUserText.label: 'Tel.No.'
@UI: {
lineItem: [{ position: 60}],
identification: [{position: 60}],
fieldGroup: [{qualifier: 'Basic'}]
}
@ObjectModel.mandatory: true
Telnum,
@EndUserText.label: 'E-Mail'
@UI: {
lineItem: [{ position: 70}],
identification: [{position: 70}],
fieldGroup: [{qualifier: 'Basic'}]
}
@ObjectModel.mandatory: true
Email,
@EndUserText.label: 'Reason'
@Consumption.valueHelpDefinition: [ {
entity: {
name: 'ZXXX_CDS_VIS_REASON_DOM',
element: 'Low'
}
} ]
@ObjectModel.text.element: ['ReasonText']
@UI: {
lineItem: [{ position: 80}],
identification: [{position: 80}],
fieldGroup: [{qualifier: 'Basic'}]
}
@ObjectModel.mandatory: true
@UI.textArrangement: #TEXT_ONLY
@UI.selectionField: [{ position: 20 }]
Reason,
_ReasonValueHelp.Text as ReasonText,
@EndUserText.label: 'Host Information'
@UI: {
lineItem: [{ position: 90}],
identification: [{position: 90}],
fieldGroup: [{qualifier: 'Basic'}]
}
@ObjectModel.mandatory: true
Vhost,
@EndUserText.label: 'Date'
@UI: {
lineItem: [{ position: 100}],
identification: [{position: 100}],
fieldGroup: [{qualifier: 'Basic'}]
}
@ObjectModel.mandatory: true
Vdate,
@EndUserText.label: 'Check-In Time'
@UI: {
lineItem: [{ position: 110}],
identification: [{position: 110}],
fieldGroup: [{qualifier: 'Basic'}]
}
@ObjectModel.mandatory: true
Intime,
@EndUserText.label: 'Check-Out Time'
@UI: {
lineItem: [{ position: 120}],
identification: [{position: 120}],
fieldGroup: [{qualifier: 'Basic'}]
}
@ObjectModel.mandatory: true
Outtime,
@EndUserText.label: 'Status'
@Consumption.valueHelpDefinition: [ {
entity: {
name: 'ZXXX_CDS_VIS_STATUS_DOM',
element: 'Low'
}
} ]
@ObjectModel.text.element: ['StatusText']
@UI: {
lineItem: [{ position: 81}],
identification: [{position: 81}],
fieldGroup: [{qualifier: 'Basic'}]
}
@ObjectModel.mandatory: true
@UI.textArrangement: #TEXT_ONLY
@UI.selectionField: [{ position: 30 }]
Status,
}
base cds for fetching fixed values of any domain
@AbapCatalog.sqlViewName: 'ZXXXCDSDOMVAL'
@AbapCatalog.compiler.compareFilter: true
@AbapCatalog.preserveKey: true
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Domain Value Fetch'
define view ZXXX_CDS_DOMVAL
as select from dd07l as FixedValue
left outer join dd07t as ValueText on FixedValue.domname = ValueText.domname
and FixedValue.domvalue_l = ValueText.domvalue_l
and FixedValue.as4local = ValueText.as4local
{
@UI.hidden
key FixedValue.domname as DomainName,
@UI.hidden
key FixedValue.as4local as Status,
@Search.defaultSearchElement: true
@Search.fuzzinessThreshold: 0.8
key FixedValue.domvalue_l as Low,
@Semantics.text: true -- identifies the text field
ValueText.ddtext as Text
}
where
FixedValue.as4local = 'A' --Active
and ValueText.ddlanguage = $session.system_language
projection view for fetching fixed values of our domains
@AbapCatalog.sqlViewName: 'ZXXXVISRSNDOM'
@AbapCatalog.compiler.compareFilter: true
@AbapCatalog.preserveKey: true
@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Reason Domain Values'
@Search.searchable: true
@ObjectModel.resultSet.sizeCategory: #XS
define view ZXXX_CDS_VIS_REASON_DOM
as select from ZXXX_CDS_DOMVAL as DomValues
{
@UI.hidden
key DomValues.DomainName,
@EndUserText.label: 'Status' -- Custom label text
@UI.textArrangement: #TEXT_ONLY
@ObjectModel.text.element: [ 'Text' ]
key DomValues.Low,
@Semantics.text: true -- identifies the text field
@Search.defaultSearchElement: true
@Search.fuzzinessThreshold: 0.8
@UI.hidden: true
DomValues.Text
}
where
DomValues.DomainName = 'ZXXX_DO_VIS_REASON'
Register the service in the backend on Tcode – /n/IWFND/MAINT_SERVICE
Select the Gateway client to test the Odata Service.
It’s time to create Fiori Elements App by using VSCode.
Select Application type and template
Select Data source and Service
Select Main Entity
Provide necessary details
App has been created so We can check CRUD operations of our app.
Open BOBX Tcode and BO
Create New Action
Enter action name and description then create Implementing Class by double clicking after entering Implementing Class Name
Open execute method of Implementing Class
Purpose of this action is setting status done of selected rows. According to this purpose, firstly we need to read the data we want to change, then update the status with call the UPDATE method of io_modify.
DATA:
lt_head TYPE ztxxx_cds_vis_02,
lv_nextnum TYPE numc10.
io_read->retrieve(
EXPORTING
iv_node = is_ctx-node_key
it_key = it_key
iv_fill_data = abap_true
IMPORTING
et_data = lt_head
).
LOOP AT lt_head REFERENCE INTO DATA(lr_head).
IF lr_head->status NE 'D'.
lr_head->status = 'D'.
io_modify->update(
EXPORTING
iv_node = is_ctx-node_key
iv_key = lr_head->key
iv_root_key = lr_head->root_key
is_data = lr_head
).
ENDIF.
ENDLOOP.
Open Eclipse and Add below code block to our Projection CDS View.
@UI: {
lineItem: [{ type: #FOR_ACTION, position: 1,
dataAction: 'BOPF:SET_STATUS_DONE',
label: 'Set Status: Done' }],
identification: [{type: #FOR_ACTION,
position: 1,
dataAction: 'BOPF:SET_STATUS_DONE',
label: 'Set Status: Done' }]
}
Creating a Fiori Elements app for using CDS annotations and the BOPF Framework is a powerful way to build apps without writing extensive UI5 code. If you’re interested in exploring more about this or diving deeper into modern ABAP development, let’s chat in the comments! Also, consider checking out SAP RAP (Restful ABAP Programming) for further insights into streamlined development approaches.