RAP Projection Views: Multi-UI Scenarios with One Business Object

Category
RAP
Published
Author
Johannes

Projection Views form the top layer of the RAP architecture and enable exposing a single Business Object for different use cases. With projections, you can build different UIs, authorizations, and behaviors on the same data model.

The Projection Layer Concept

In RAP, the architecture follows the Layered Architecture Pattern:

┌─────────────────────────────────────────────────────────┐
│ Service Layer │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Service A │ │ Service B │ │ Service C │ │
│ │ (Fiori App) │ │ (API) │ │ (Analytics) │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
├─────────┼────────────────┼────────────────┼─────────────┤
│ │ Projection Layer (C_*) │ │
│ ┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐ │
│ │ ZC_Travel │ │ ZC_TravelAPI│ │ ZC_TravelRpt│ │
│ │ (UI-focus) │ │ (minimal) │ │ (readonly) │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
├─────────┼────────────────┼────────────────┼─────────────┤
│ │ Business Object Layer (I_*) │ │
│ │ ┌────────▼────────┐ │ │
│ └───────► ZI_Travel ◄───────┘ │
│ │ (Core logic) │ │
│ └─────────────────┘ │
├─────────────────────────────────────────────────────────┤
│ Database Layer │
└─────────────────────────────────────────────────────────┘

Naming Conventions

PrefixLayerDescription
I_ or ZI_InterfaceBusiness Object with complete logic
C_ or ZC_ConsumptionProjection for specific use case
R_ or ZR_RootAlternative for interface views

Why Projection Views?

Use Cases for Multi-UI Scenarios

  1. Different User Groups

    • Clerk: Full access to all fields
    • Manager: Only approval and overview
    • External partners: Restricted view
  2. Different Devices

    • Desktop: Feature-rich Fiori app
    • Mobile: Simplified view
  3. Different Purposes

    • Transactional app: CRUD operations
    • Reporting: Read-only access
    • API: Technical interface

Creating Projection CDS Views

Interface View (Base)

@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Travel - Interface View'
define root view entity ZI_Travel
as select from ztravel
composition [0..*] of ZI_Booking as _Booking
association [1..1] to ZI_Agency as _Agency on $projection.AgencyId = _Agency.AgencyId
association [1..1] to ZI_Customer as _Customer on $projection.CustomerId = _Customer.CustomerId
{
key travel_id as TravelId,
agency_id as AgencyId,
customer_id as CustomerId,
begin_date as BeginDate,
end_date as EndDate,
@Semantics.amount.currencyCode: 'CurrencyCode'
total_price as TotalPrice,
currency_code as CurrencyCode,
description as Description,
status as Status,
@Semantics.user.createdBy: true
created_by as CreatedBy,
@Semantics.systemDateTime.createdAt: true
created_at as CreatedAt,
@Semantics.user.lastChangedBy: true
last_changed_by as LastChangedBy,
@Semantics.systemDateTime.lastChangedAt: true
last_changed_at as LastChangedAt,
// Associations
_Booking,
_Agency,
_Customer
}

Projection View for Fiori UI

@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Travel - Fiori App'
@Metadata.allowExtensions: true
define root view entity ZC_Travel
provider contract transactional_query
as projection on ZI_Travel
{
key TravelId,
AgencyId,
CustomerId,
BeginDate,
EndDate,
TotalPrice,
CurrencyCode,
Description,
Status,
// Calculated fields only for this projection
case Status
when 'A' then 'Accepted'
when 'X' then 'Rejected'
when 'O' then 'Open'
else 'Unknown'
end as StatusText,
CreatedBy,
CreatedAt,
LastChangedBy,
LastChangedAt,
// Expose associations
_Booking : redirected to composition child ZC_Booking,
_Agency,
_Customer
}

Projection View for API Consumers

@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Travel - API'
define root view entity ZC_TravelAPI
provider contract transactional_query
as projection on ZI_Travel
{
key TravelId,
AgencyId,
CustomerId,
BeginDate,
EndDate,
TotalPrice,
CurrencyCode,
Status,
// No UI-specific fields
// No calculated fields for status text
LastChangedAt,
_Booking : redirected to composition child ZC_BookingAPI
}

Projection View for Reporting (Read-Only)

@AccessControl.authorizationCheck: #CHECK
@EndUserText.label: 'Travel - Reporting'
@ObjectModel.usageType: {
serviceQuality: #A,
sizeCategory: #L,
dataClass: #TRANSACTIONAL
}
define root view entity ZC_TravelReport
provider contract transactional_query
as projection on ZI_Travel
{
key TravelId,
AgencyId,
_Agency.Name as AgencyName,
CustomerId,
_Customer.LastName as CustomerName,
BeginDate,
EndDate,
// Calculate duration
dats_days_between( BeginDate, EndDate ) as TravelDuration,
TotalPrice,
CurrencyCode,
Status,
CreatedAt,
// For aggregations
@DefaultAggregation: #SUM
TotalPrice as SumTotalPrice,
@DefaultAggregation: #COUNT
cast( 1 as abap.int4 ) as TravelCount
}

Behavior Projection

Each Projection View needs its own Behavior Projection that adapts the Business Object behavior for this use case.

Interface Behavior Definition (Base)

managed implementation in class zbp_i_travel unique;
strict ( 2 );
define behavior for ZI_Travel alias Travel
persistent table ztravel
lock master
authorization master ( instance )
etag master LastChangedAt
{
create;
update;
delete;
field ( readonly ) TravelId;
field ( mandatory ) AgencyId, CustomerId, BeginDate, EndDate;
field ( readonly ) CreatedBy, CreatedAt, LastChangedBy, LastChangedAt;
action acceptTravel result [1] $self;
action rejectTravel result [1] $self;
determination setTravelId on save { create; }
validation validateDates on save { create; update; }
validation validateStatus on save { create; update; }
association _Booking { create; }
mapping for ztravel
{
TravelId = travel_id;
AgencyId = agency_id;
CustomerId = customer_id;
BeginDate = begin_date;
EndDate = end_date;
TotalPrice = total_price;
CurrencyCode = currency_code;
Description = description;
Status = status;
CreatedBy = created_by;
CreatedAt = created_at;
LastChangedBy = last_changed_by;
LastChangedAt = last_changed_at;
}
}
define behavior for ZI_Booking alias Booking
persistent table zbooking
lock dependent by _Travel
authorization dependent by _Travel
etag master LastChangedAt
{
update;
delete;
field ( readonly ) BookingId, TravelId;
association _Travel;
}

Behavior Projection for Fiori App

The projection exposes all operations for the Fiori application:

projection;
strict ( 2 );
define behavior for ZC_Travel alias Travel
{
use create;
use update;
use delete;
use action acceptTravel;
use action rejectTravel;
use association _Booking { create; }
}
define behavior for ZC_Booking alias Booking
{
use update;
use delete;
use association _Travel;
}

Behavior Projection for API (Restricted)

The API projection exposes only selected operations:

projection;
strict ( 2 );
define behavior for ZC_TravelAPI alias Travel
{
use create;
use update;
// No delete - API cannot delete
use action acceptTravel;
// No rejectTravel for API
use association _Booking { create; }
}
define behavior for ZC_BookingAPI alias Booking
{
use update;
// No delete for bookings via API
use association _Travel;
}

Behavior Projection for Reporting (Read-Only)

The reporting projection exposes no modification operations:

projection;
strict ( 2 );
define behavior for ZC_TravelReport alias Travel
{
// No operations - read-only access
// Read is automatically supported
}

Metadata Extensions

Metadata Extensions enable different UI annotations per projection without changing the projection view itself.

Metadata Extension for Fiori App

@Metadata.layer: #CORE
annotate entity ZC_Travel with
{
@UI.facet: [
{ id: 'TravelHeader',
purpose: #STANDARD,
type: #IDENTIFICATION_REFERENCE,
label: 'Travel Details',
position: 10 },
{ id: 'Bookings',
purpose: #STANDARD,
type: #LINEITEM_REFERENCE,
label: 'Bookings',
position: 20,
targetElement: '_Booking' }
]
@UI: {
headerInfo: {
typeName: 'Travel',
typeNamePlural: 'Travels',
title: { type: #STANDARD, value: 'Description' },
description: { type: #STANDARD, value: 'TravelId' }
}
}
@UI: {
lineItem: [{ position: 10, importance: #HIGH }],
identification: [{ position: 10 }],
selectionField: [{ position: 10 }]
}
TravelId;
@UI: {
lineItem: [{ position: 20, importance: #HIGH }],
identification: [{ position: 20 }],
selectionField: [{ position: 20 }]
}
@Consumption.valueHelpDefinition: [{ entity: { name: 'ZI_Agency', element: 'AgencyId' } }]
AgencyId;
@UI: {
lineItem: [{ position: 30, importance: #HIGH }],
identification: [{ position: 30 }],
selectionField: [{ position: 30 }]
}
@Consumption.valueHelpDefinition: [{ entity: { name: 'ZI_Customer', element: 'CustomerId' } }]
CustomerId;
@UI: {
lineItem: [{ position: 40, importance: #MEDIUM }],
identification: [{ position: 40 }]
}
BeginDate;
@UI: {
lineItem: [{ position: 50, importance: #MEDIUM }],
identification: [{ position: 50 }]
}
EndDate;
@UI: {
lineItem: [{ position: 60, importance: #HIGH, criticality: 'StatusCriticality' }],
identification: [{ position: 60, criticality: 'StatusCriticality' }],
selectionField: [{ position: 40 }]
}
Status;
@UI: {
lineItem: [{ position: 70, importance: #HIGH }],
identification: [{ position: 70 }]
}
TotalPrice;
@UI.identification: [
{ type: #FOR_ACTION, dataAction: 'acceptTravel', label: 'Accept', position: 10 },
{ type: #FOR_ACTION, dataAction: 'rejectTravel', label: 'Reject', position: 20 }
]
StatusText;
}

Metadata Extension for Manager View

@Metadata.layer: #CORE
annotate entity ZC_TravelManager with
{
@UI.facet: [
{ id: 'Overview',
purpose: #STANDARD,
type: #IDENTIFICATION_REFERENCE,
label: 'Overview',
position: 10 }
]
@UI: {
headerInfo: {
typeName: 'Approval',
typeNamePlural: 'Approvals',
title: { type: #STANDARD, value: 'Description' }
}
}
// Only relevant fields for managers
@UI.lineItem: [{ position: 10, importance: #HIGH }]
TravelId;
@UI.lineItem: [{ position: 20, importance: #HIGH }]
CustomerName;
@UI.lineItem: [{ position: 30, importance: #HIGH }]
TotalPrice;
@UI.lineItem: [{ position: 40, importance: #HIGH, criticality: 'StatusCriticality' }]
Status;
// Prominent action buttons
@UI.lineItem: [
{ type: #FOR_ACTION, dataAction: 'acceptTravel', label: 'Approve', position: 50 },
{ type: #FOR_ACTION, dataAction: 'rejectTravel', label: 'Reject', position: 60 }
]
Description;
}

Feature Control per Projection

Through Dynamic Feature Control, you can implement different rules per projection. The logic is defined centrally in the Behavior Implementation but controlled via the Behavior Projection.

Example: Different Feature Control

In the Interface Behavior, you define the features:

define behavior for ZI_Travel alias Travel
{
// ...
// Features for differentiated control
action ( features: instance ) acceptTravel result [1] $self;
action ( features: instance ) rejectTravel result [1] $self;
delete ( features: instance );
field ( features: instance ) Status;
}

The implementation checks the context:

CLASS zbp_i_travel IMPLEMENTATION.
METHOD get_instance_features.
READ ENTITIES OF ZI_Travel IN LOCAL MODE
ENTITY Travel
FIELDS ( Status )
WITH CORRESPONDING #( keys )
RESULT DATA(travels).
LOOP AT travels INTO DATA(travel).
APPEND VALUE #(
%tky = travel-%tky
" Status-dependent features
%action-acceptTravel = COND #(
WHEN travel-Status = 'O' THEN if_abap_behv=>fc-o-enabled
ELSE if_abap_behv=>fc-o-disabled )
%action-rejectTravel = COND #(
WHEN travel-Status = 'O' THEN if_abap_behv=>fc-o-enabled
ELSE if_abap_behv=>fc-o-disabled )
" Delete only for open travels
%delete = COND #(
WHEN travel-Status = 'O' THEN if_abap_behv=>fc-o-enabled
ELSE if_abap_behv=>fc-o-disabled )
" Status field read-only when not open
%field-Status = COND #(
WHEN travel-Status = 'O' THEN if_abap_behv=>fc-f-unrestricted
ELSE if_abap_behv=>fc-f-read_only )
) TO result.
ENDLOOP.
ENDMETHOD.
ENDCLASS.

In the API projection, you can then completely omit features or override them by adjusting the Behavior Projection accordingly.

Service Definition and Binding

For each projection, you create a separate service:

Service Definition for Fiori App

@EndUserText.label: 'Travel - Fiori Service'
define service ZUI_TRAVEL_O4 {
expose ZC_Travel as Travel;
expose ZC_Booking as Booking;
expose ZI_Agency as Agency;
expose ZI_Customer as Customer;
}

Service Definition for API

@EndUserText.label: 'Travel - API Service'
define service ZAPI_TRAVEL_O4 {
expose ZC_TravelAPI as Travel;
expose ZC_BookingAPI as Booking;
}

Service Binding

For each service, you create a Service Binding with the appropriate protocol:

BindingProtocolUsage
ZUI_TRAVEL_O4_V4OData V4 - UIFiori Elements
ZAPI_TRAVEL_O4_V4OData V4 - Web APIREST clients
ZUI_TRAVEL_O2OData V2Legacy Fiori apps

Best Practices

1. Clear Separation of Layers

- Interface View: Complete data model, no UI annotations
- Projection View: Application-specific fields, no business logic
- Metadata Extension: UI annotations separate from view

2. Consistent Naming

ZI_Travel → Interface (Base)
ZC_Travel → Fiori Consumption
ZC_TravelAPI → API Consumption
ZC_TravelReport → Reporting Consumption
ZC_TravelManager → Manager Consumption

3. Redirect Associations Correctly

When projecting compositions, the association must be redirected to the corresponding child projection:

_Booking : redirected to composition child ZC_Booking

4. Mind the Provider Contract

The provider contract defines the purpose of the projection:

ContractUsage
transactional_queryStandard for UI and API
transactional_interfaceFor A2A integration
analytical_queryFor Embedded Analytics

Avoiding Common Errors

Error 1: Missing Behavior Projection

" WRONG: Only CDS Projection without Behavior
define root view entity ZC_Travel as projection on ZI_Travel { ... }
" CORRECT: CDS Projection WITH Behavior Projection
" 1. Create CDS View
" 2. Create Behavior Projection
" 3. Service Definition and Binding

Error 2: Association Not Redirected

" WRONG: Association still points to Interface
_Booking,
" CORRECT: Redirect association to Projection
_Booking : redirected to composition child ZC_Booking,

Error 3: Inconsistent Hierarchy

" WRONG: Child projection points to interface parent
define view entity ZC_Booking as projection on ZI_Booking
{
_Travel, " Points to ZI_Travel instead of ZC_Travel
}
" CORRECT: Consistent projection hierarchy
define view entity ZC_Booking as projection on ZI_Booking
{
_Travel : redirected to parent ZC_Travel,
}

Further Reading

Conclusion

Projection Views are a powerful concept for exposing a Business Object for different use cases. Through clean separation of Interface Layer, Projection Layer, and Service Layer, you achieve:

  • Reusability: One Business Object, multiple UIs
  • Maintainability: Changes to interface affect all projections
  • Flexibility: Different features, fields, and annotations per use case
  • Security: Restricted operations for specific user groups

With the combination of Projection Views, Behavior Projections, and Metadata Extensions, you have full control over how your Business Object is presented and used in different contexts.