RAP Basics
Architect / Cross-trackintermediate

RAP Build-Along Part 5: Projection Views and Fiori Elements UI Annotations

Key takeaway

Part 5 of the RAP Bootcamp builds the consumer layer of the Travel + Booking app: projection views with redirected composition and parent association, a projection behavior that exposes only intended operations and actions, and metadata extensions that produce a Fiori Elements List Report and Object Page with a Booking table.

Explanation

📌 KEY POINTS
✅ Projection views (ZC_) define what one app exposes; redirect compositions.

✅ Projection BDEF: "use" the operations and actions the app needs.

✅ UI annotations in metadata extensions: list, filters, facets, action buttons.

💡 Preview from the service binding to see the result immediately.

▶ SAP FIORI ELEMENTS WITH RAP: PROJECTION VIEWS AND UI ANNOTATIONS (PART 5)

━━━━━━━━━━━━━━━━━━━━
▶ WHAT YOU WILL BUILD

| Artifact | Name |
|---|---|

| Projection root view | ZC_RAP_Travel |

| Projection child view | ZC_RAP_Booking |

| Projection behavior | ZC_RAP_Travel (behavior definition, type Projection) |

| Metadata extension (root) | ZC_RAP_Travel |

| Metadata extension (child) | ZC_RAP_Booking |

At the end, once Part 6 exposes the service, the preview shows a Travel list with filters, an object page with header, general information, prices, and a Booking table, plus Accept/Reject buttons.

━━━━━━━━━━━━━━━━━━━━
▶ PREREQUISITES / PREVIOUS PART

  • [Part 4](/lessons/rap-bootcamp-part-4-behavior-implementation): working BO ZI_RAP_Travel with determinations, validations, actions, feature control and authorization.

━━━━━━━━━━━━━━━━━━━━
▶ WHY PROJECTIONS EXIST

The interface layer (ZI_) is the reusable BO: all fields, all behavior. A projection (ZC_) is a consumer-specific view of it — for one app or one API. It can:

  • hide fields the app doesn't need,
  • add consumer-specific annotations (search, value help, UI),
  • expose a subset of operations and actions,
  • rename or denormalize text fields for display.

It cannot add business logic. All rules stay in the BO. One BO can have several projections (for example, a processor UI and a read-only analyst UI, or a UI and a Web API), each exposing what that consumer may do.

━━━━━━━━━━━━━━━━━━━━
▶ STEP 1 — PROJECTION ROOT VIEW

New *Data Definition* ZC_RAP_Travel, template *Define Projection View*, referenced object ZI_RAP_Travel.

  • define root view entity ZC_RAP_Travel provider contract transactional_query as projection on ZI_RAP_Travel — provider contract transactional_query (release-dependent; required in current releases for UI projections) states that this projection is consumed transactionally via OData.
  • @Metadata.allowExtensions: true allows the metadata extension.
  • @Search.searchable: true and @Search.defaultSearchElement: true on a few fields enable the Fiori search box.
  • redirected to composition child ZC_RAP_Booking — the composition must point to the projection child, not the interface child. Otherwise the projection tree breaks.

━━━━━━━━━━━━━━━━━━━━
▶ STEP 2 — PROJECTION CHILD VIEW

ZC_RAP_Booking as projection on ZI_RAP_Booking:

  • define view entity (not root).
  • _Travel : redirected to parent ZC_RAP_Travel — parent association redirected to the projection root.
  • Field list can be narrower than the interface; keep key, parent key, and anything shown in the UI.

Activation: mass-activate ZC_RAP_Travel and ZC_RAP_Booking together (same cycle as Part 2).

━━━━━━━━━━━━━━━━━━━━
▶ STEP 3 — VALUE HELP (NOTE)

Add @Consumption.valueHelpDefinition on fields such as CurrencyCode pointing to a released view (for example I_Currency), or on CustomerID to a customer view available in your landscape. In ABAP Cloud, only released views may be used. If no released view exists for your domain, build a small custom value-help CDS on your own tables. Value helps are consumer annotations; they never replace validations in the BO.

━━━━━━━━━━━━━━━━━━━━
▶ STEP 4 — PROJECTION BEHAVIOR

Right-click ZC_RAP_Travel → *New Behavior Definition* → type Projection.

┏━━ CODE ━━━━━━━━━━━━
┃ projection;

┃ strict ( 2 );

┃ use draft;

┗━━━━━━━━━━━━━━━━━━━

Then per entity use create; use update; use delete; use action ...; use association .... Anything not listed with use is not exposed through this projection, even though the BO supports it. This is how you restrict operations: for example, a read-only projection would list no use operations, and a processor app might expose acceptTravel but not rejectTravel.

Draft actions must be exposed explicitly: use action Edit; use action Activate; use action Discard; use action Resume; use action Prepare; (in some releases with use draft these are required explicitly — ADT syntax check tells you).

━━━━━━━━━━━━━━━━━━━━
▶ STEP 5 — METADATA EXTENSION DESIGN

Metadata extensions keep UI annotations out of the data model, so the projection stays readable and layers can be adjusted. Create *New Metadata Extension* ZC_RAP_Travel with @Metadata.layer: #CORE (use #CUSTOMER / #PARTNER layers when extending delivered content).

Key annotations:

| Annotation | Effect |
|---|---|

| @UI.headerInfo | Object page title, type name, list title |

| @UI.facet | Object page sections: identification, field groups, child table |

| @UI.lineItem | Columns in the list report / child table; also action buttons |

| @UI.identification | Fields in the "General" section; also object page actions |

| @UI.selectionField | Filter bar fields |

| @UI.fieldGroup | Named groups used by facets |

| @UI.hidden | Hide technical fields (UUIDs) |

━━━━━━━━━━━━━━━━━━━━
▶ STEP 6 — FACETS AND THE BOOKING TABLE

The facet list has:

  1. A collection facet GeneralInfo containing an identification reference and a field-group reference for Prices.
  2. A #LINEITEM_REFERENCE facet with targetElement: '_Booking' — this renders the Booking table on the object page, using @UI.lineItem from ZC_RAP_Booking's metadata extension.

The Booking metadata extension has its own headerInfo and facets, so clicking a booking row opens a Booking object page.

━━━━━━━━━━━━━━━━━━━━
▶ STEP 7 — ACTION PLACEMENT

  • @UI.lineItem: [{ type: #FOR_ACTION, dataAction: 'acceptTravel', label: 'Accept' }] → button on the list report toolbar (acts on selected rows).
  • @UI.identification: [{ type: #FOR_ACTION, dataAction: 'acceptTravel', label: 'Accept' }] → button on the object page header.

dataAction must match the action name in the projection behavior exactly. The button's enabled state comes from get_instance_features (Part 4) — no UI code needed.

━━━━━━━━━━━━━━━━━━━━
▶ STEP 8 — CRITICALITY AND STATUS

Add a virtual or calculated criticality in the projection (case OverallStatus when 'A' then 3 when 'X' then 1 else 2 end as StatusCriticality) and reference it with criticality: 'StatusCriticality' in lineItem. Calculated fields in projections are fine — they are presentation, not business logic.

━━━━━━━━━━━━━━━━━━━━
▶ PREVIEW CHECKPOINT

Service exposure happens in Part 6, but you can verify now:

  1. Activate all five artifacts without errors.
  2. Data Preview on ZC_RAP_Travel — navigate _Booking; it should return ZC_RAP_Booking rows.
  3. ADT *Element Info* (F2) on ZC_RAP_Travel shows annotations merged from the metadata extension.

After Part 6's binding, the Fiori preview should show: filter bar (TravelID, CustomerID, OverallStatus), list with status colors, Create button, object page with General/Prices sections and Booking table, Edit/Save/Discard (draft), Accept/Reject buttons that disable per status.

━━━━━━━━━━━━━━━━━━━━
▶ METADATA TROUBLESHOOTING

| Symptom | Cause | Fix |
|---|---|---|

| Object page empty | No @UI.facet | Add facets |

| Booking table missing | Facet targetElement wrong or composition not redirected | Use _Booking, redirect composition |

| Action button missing | dataAction typo or action not used in projection BDEF | Match names exactly |

| Button always disabled | Feature control returns disabled / authorization | Debug get_instance_features |

| Annotations ignored | @Metadata.allowExtensions missing | Add to projection view |

| Fields editable that shouldn't be | Field control in BO, not UI | Fix BDEF readonly, not annotations |

| Create button missing | use create absent or global auth denied | Check projection BDEF and auth |

━━━━━━━━━━━━━━━━━━━━
▶ CHECKPOINT

  • [ ] ZC_RAP_Travel (root, transactional_query) and ZC_RAP_Booking active with redirected composition/parent.
  • [ ] Projection behavior exposes CRUD, create-by-association, draft actions, Accept/Reject.
  • [ ] Metadata extensions define headerInfo, selection fields, line items, identification, facets, Booking table.
  • [ ] No business logic in projection or metadata extension.

━━━━━━━━━━━━━━━━━━━━
▶ TRY IT YOURSELF

Create a second projection ZC_RAP_TravelReadOnly with its own projection behavior that exposes no modifying operations, and a compact metadata extension. You will expose it in Part 6 as a separate read-only service — same BO, different contract.

━━━━━━━━━━━━━━━━━━━━
▶ INTERVIEW TAKEAWAY

"The projection layer is the consumer contract. The projection views redirect composition and parent associations to projection entities, the projection behavior uses use to expose only intended operations and actions, and metadata extensions hold UI annotations — headerInfo, lineItem, identification, selectionField and facets — including the child table and action buttons. Business rules and field control stay in the BO."

━━━━━━━━━━━━━━━━━━━━
▶ NEXT PART

[Part 6: Service Definition, OData V4 Binding and End-to-End Testing](/lessons/rap-bootcamp-part-6-odata-v4-eml-testing)

Code example

ABAP Code
-- ============================================================-- Projection root ZC_RAP_Travel-- (provider contract is release-dependent; required in current releases)-- ============================================================@AccessControl.authorizationCheck: #CHECK@Metadata.allowExtensions: true@Search.searchable: true@EndUserText.label: 'Travel - projection for UI'define root view entity ZC_RAP_Travel  provider contract transactional_query  as projection on ZI_RAP_Travel{  key TravelUUID,      @Search.defaultSearchElement: true      TravelID,      AgencyID,      @Search.defaultSearchElement: true      CustomerID,      BeginDate,      EndDate,      @Semantics.amount.currencyCode: 'CurrencyCode'      BookingFee,      @Semantics.amount.currencyCode: 'CurrencyCode'      TotalPrice,      @Consumption.valueHelpDefinition: [{ entity: { name: 'I_Currency', element: 'Currency' } }]      CurrencyCode,      Description,      OverallStatus,      case OverallStatus        when 'A' then 3        when 'X' then 1        else 2      end as StatusCriticality,      CreatedBy,      CreatedAt,      LastChangedBy,      LastChangedAt,      LocalLastChangedAt,       _Booking : redirected to composition child ZC_RAP_Booking,      _Currency} -- ============================================================-- Projection child ZC_RAP_Booking-- ============================================================@AccessControl.authorizationCheck: #CHECK@Metadata.allowExtensions: true@EndUserText.label: 'Booking - projection for UI'define view entity ZC_RAP_Booking  as projection on ZI_RAP_Booking{  key BookingUUID,      ParentUUID,      BookingID,      BookingDate,      CustomerID,      CarrierID,      ConnectionID,      FlightDate,      @Semantics.amount.currencyCode: 'CurrencyCode'      FlightPrice,      @Consumption.valueHelpDefinition: [{ entity: { name: 'I_Currency', element: 'Currency' } }]      CurrencyCode,      BookingStatus,      LocalLastChangedAt,       _Travel : redirected to parent ZC_RAP_Travel,      _Currency} " ============================================================" Projection behavior ZC_RAP_Travel" ============================================================projection;strict ( 2 );use draft; define behavior for ZC_RAP_Travel alias Traveluse etag{  use create;  use update;  use delete;   use action acceptTravel;  use action rejectTravel;   use action Edit;  use action Activate;  use action Discard;  use action Resume;  use action Prepare;   use association _Booking { create; with draft; }} define behavior for ZC_RAP_Booking alias Bookinguse etag{  use update;  use delete;   use association _Travel { with draft; }} -- ============================================================-- Metadata extension ZC_RAP_Travel-- ============================================================@Metadata.layer: #CORE@UI: {  headerInfo: { typeName: 'Travel', typeNamePlural: 'Travels',                title: { type: #STANDARD, value: 'TravelID' },                description: { value: 'Description' } },  presentationVariant: [{ sortOrder: [{ by: 'TravelID', direction: #DESC }] }]}annotate entity ZC_RAP_Travel with{  @UI.facet: [    { id: 'GeneralInfo', type: #COLLECTION, label: 'General Information', position: 10 },    { id: 'Basic', parentId: 'GeneralInfo', type: #IDENTIFICATION_REFERENCE,      label: 'Basic Data', position: 10 },    { id: 'Prices', parentId: 'GeneralInfo', type: #FIELDGROUP_REFERENCE,      targetQualifier: 'Prices', label: 'Prices', position: 20 },    { id: 'Booking', type: #LINEITEM_REFERENCE, label: 'Bookings',      targetElement: '_Booking', position: 20 }  ]   @UI.hidden: true  TravelUUID;   @UI: { lineItem:       [ { position: 10, importance: #HIGH },                           { type: #FOR_ACTION, dataAction: 'acceptTravel', label: 'Accept' },                           { type: #FOR_ACTION, dataAction: 'rejectTravel', label: 'Reject' } ],         identification: [ { position: 10 },                           { type: #FOR_ACTION, dataAction: 'acceptTravel', label: 'Accept' },                           { type: #FOR_ACTION, dataAction: 'rejectTravel', label: 'Reject' } ],         selectionField: [ { position: 10 } ] }  TravelID;   @UI: { lineItem: [ { position: 20 } ], identification: [ { position: 20 } ] }  AgencyID;   @UI: { lineItem: [ { position: 30, importance: #HIGH } ],         identification: [ { position: 30 } ],         selectionField: [ { position: 20 } ] }  CustomerID;   @UI: { lineItem: [ { position: 40 } ], identification: [ { position: 40 } ] }  BeginDate;   @UI: { lineItem: [ { position: 50 } ], identification: [ { position: 50 } ] }  EndDate;   @UI.fieldGroup: [ { qualifier: 'Prices', position: 10 } ]  BookingFee;   @UI: { lineItem: [ { position: 60 } ],         fieldGroup: [ { qualifier: 'Prices', position: 20 } ] }  TotalPrice;   @UI.identification: [ { position: 60 } ]  Description;   @UI: { lineItem: [ { position: 70, criticality: 'StatusCriticality', importance: #HIGH } ],         identification: [ { position: 70, criticality: 'StatusCriticality' } ],         selectionField: [ { position: 30 } ] }  OverallStatus;   @UI.hidden: true  StatusCriticality;   @UI.hidden: true  LocalLastChangedAt;} -- ============================================================-- Metadata extension ZC_RAP_Booking-- ============================================================@Metadata.layer: #CORE@UI.headerInfo: { typeName: 'Booking', typeNamePlural: 'Bookings',                  title: { type: #STANDARD, value: 'BookingID' } }annotate entity ZC_RAP_Booking with{  @UI.facet: [ { id: 'Booking', type: #IDENTIFICATION_REFERENCE,                 label: 'Booking', position: 10 } ]   @UI.hidden: true  BookingUUID;  @UI.hidden: true  ParentUUID;   @UI: { lineItem: [ { position: 10 } ], identification: [ { position: 10 } ] }  BookingID;  @UI: { lineItem: [ { position: 20 } ], identification: [ { position: 20 } ] }  CarrierID;  @UI: { lineItem: [ { position: 30 } ], identification: [ { position: 30 } ] }  ConnectionID;  @UI: { lineItem: [ { position: 40 } ], identification: [ { position: 40 } ] }  FlightDate;  @UI: { lineItem: [ { position: 50 } ], identification: [ { position: 50 } ] }  FlightPrice;  @UI: { lineItem: [ { position: 60 } ], identification: [ { position: 60 } ] }  BookingStatus;  @UI.hidden: true  LocalLastChangedAt;}

Real project scenario

A team built one projection and exposed every BO operation to both a processor Fiori app and an external integration. Integration users could suddenly trigger the Reject action. The fix was not UI hiding: they created a second projection with its own projection behavior that used only create and read-by-association, exposed it through a separate service, and kept the BO unchanged. Consumer contracts became explicit and testable.

Common mistakes

  • Not redirecting the composition/parent association to projection entities.
  • Forgetting use for draft actions or custom actions, so buttons never appear.
  • dataAction names not matching the action name exactly.
  • Hiding a button in the UI and assuming it is secured — control access with feature control and authorization in the BO.
  • Putting field control into UI annotations instead of the BDEF.
  • Missing @Metadata.allowExtensions: true so the metadata extension is ignored.
  • Putting calculations with business meaning into projections instead of determinations.

Best practices

  • One projection per consumer contract; never expose the interface layer directly.
  • Expose only needed operations with use.
  • Keep UI annotations in metadata extensions, not in the projection view.
  • Hide technical keys with @UI.hidden.
  • Drive button enablement through instance features, not UI logic.
  • Use released views for value help in ABAP Cloud.
  • Mass-activate projection root and child together.

Interview angle

Expect: "Why do we need projections if the interface view has all fields?" — consumer-specific contract, restricted operations, UI annotations, multiple consumers on one BO. "What does use in a projection behavior do?" "Where do you place an action button in list vs object page?" "What is provider contract transactional_query?"

Practice this lesson

Quick check

1. A new field is in the projection and $metadata, but the Fiori app does not show it. What should be checked?

2. A booking price change updates the total in the backend, but the UI shows the old total. What is missing?

3. Which object decides which operations a Fiori app may use on a RAP business object?

Interview questions

1. A total field updates only after the user saves or refreshes. How do you fix it?

Answer it in your own words first, then compare.

2. A value help in a RAP Fiori app shows no entries. What do you check?

Answer it in your own words first, then compare.

Practice more with Interview AI →

Reference-verified: the steps are checked against official SAP documentation and tutorials, and object and field names in the sample code are checked for consistency. The code has not been compiled or run on an SAP system by ERPClimb. Syntax marked release-dependent varies by SAP release, so check it in your own system.

More lessons in this topic

Interview questions on this topic

Related SAP topics