Application Development Blog Posts
Learn and share on deeper, cross technology development topics such as integration and connectivity, automation, cloud extensibility, developing at scale, and security.
cancel
Showing results for 
Search instead for 
Did you mean: 
beyhan_meyrali
Active Contributor
Hi,

In this blog post, I would like to share a few thoughts on how to write better code in SAP Abap by applying MVC to code structure.

I work as an Expert Abap consultant and sometimes fellow developers are asking advices on writing better code. There are many patterns, rules in coding world to help you to write better code. MVC is one of them, and I will try to explain where MVC fits in Abap development.

Previous to Abap, I worked with many different programming languages and my favorite languages are the object oriented ones, such as Java, C# and of course Abap. I like object oriented languages, because, you can create a model of the required algorithm in small modules, you can re-use code and encapsulate complexities. I believe, it is important to write lego like, modular, reusable code. That makes coding more fun and less painfull.

Below, we will see basics of MVC and then I will try to explain, the need for MVC with a basic scenario. And finally, we will see be how it can be applied in Abap with a mvc applied report code sample.

Let's start with MVC word itself; MVC stands for Model View Controller pattern. It is used to separate layers from each other and also to make sure right code is place in right layer, in that way your application code is more robust, easier to extend, test and maintain.

And, the layers of MVC are, from top to bottom;

View -> In view layer you present/display your data. Reports, RFCs, Apis, Smartforms, ITS Dialog Screens can be part of this layer.

Controller -> In controller layer, requests are directed to related model method. Controller is the connection between view and model.  Controller can be a form procedure, method, function, just directs requests from view to Model. Should not contain any business logic in it.

Model -> Code where business logic is operated. Entry point of model can be a class method or a function. I prefer to use methods.


MVC Layers


For the sample scenario, let's imagine we are requested to create an alv report to list the pending purchase order approvals. We need to display, PO number, Person to Approve, Date Sent for Approval, Number of days since PO sent for approval.

You got the related tables and bapis, you wrote the code under report include. Great! Quickly a solution is created. You could write code in a function or in a method of a class. But, there was no time, or that was a small development or just a simple piece of code, so code is just left under report.

Now, the requirement is, if number of days of waiting is more than 3 days, you need to send a reminder email to approver. Simple, just copy the code from program and paste it to another program and create a background job to send emails in given email format. With that development, now you have 2 programs.

And after sometime, someone asked you to create pdf output to send managers of approvers. Now you copy the code again, this time place it under code block of adobe pdf interface, done! Solution created.

And after sometime again, you are requested to implement a logic to check number of days from a Z table. instead of constant duration, 3 days, days are variable according material group and safety stock quantities.  There is a saying, "When a programmer first creates his code, only he and God know how it works, a few months down the line and only God knows". You remember the last development maybe, or you may not be the developer of other programs and previous developer is gone. So You found out, there is a PDF form and a report. You have changed the code in those 2 areas. But background job is forgotten! First inconsistency.

This scenario can be extended with new requirements, new web apis, fiori apps, or for custom mobile apps. And each copy of the code is actually a base for inconsistency and extra work.

So what we have done as a fast solution, which took short time, can be very expensive at the end. Not just time while we are changing copies of code, but also in a critical scenario, inconsistencies may cause bigger problems than time. Therefore we need write code in a way, that we can re-use if we need, interface must not contain business logic ,but should be able to call function or better the method to retrieve required data and just display it.

In example above, report, email, pdf out are just interfaces. If the biz logic was implemented in a class. All interfaces could call class and get required data. And when you as a developer need to do a change, you would change that class, test that class. That is much easier, much more robust and definitely less time consuming, more extendible way of coding.

What I suggest is, try to implement MVC when you are coding, even in a small development. Once you change the way you think how to structure your programs with MVC, you will get better at that.

When you are trying to implement MVC, you may ask a question to yourself, in which layer should I put that piece of code now? Shall I keep it in interface or shall I move it to model class. Answer is simple, ask yourself, will I require that code if interface was different. If yes, place the code in lower layer, in model. In that way, you do not need to write same code for different interfaces.  If the answer is no, than just keep that code in interface.

And lets see the code samples for different types of developments, how MVC can be implemented.

 

Sample Report template;

Below template acts as two different reports/views. One of them shows data and user selects lines and creates wm transfer orders for selected ones. Or report can be set as background job with a variant and when job runs creates transfer orders for all records. Please check the code to see code reuse. One model class but two different views. That is because, business logic is not placed under report includes. If required a new program, web api, smartform or adobe form or any kind of interface can be created and can call the same model class.


I will place full code of report and related parts of class code, so that can remain here as reference.

Short info about report/view and class/model interaction ->  Report calls get_data method of class. get_data method acts as controller and forwards request to model, model object fills gt_recs table and that table is the source of ALV display.

If user_command button is clicked on screen with &createto function code, user command procedure forwards that request to model, acts as a controller.
*&---------------------------------------------------------------------*
*& Include ZBM_P_MVC_TEMPLATE- Report ZBM_P_MVC_TEMPLATE
*& Sample MVC implementation temaplate by BM
*& This report creates interface for users and also for background job
*& Calls bizobj methods to read data and displays data directly from
*& bizobj's public datas.
*&---------------------------------------------------------------------*

INCLUDE ZBM_P_MVC_TEMPLATE_TOP . " Global Data
INCLUDE ZBM_P_MVC_TEMPLATE_F01 . " Controller

END-OF-SELECTION.

PERFORM get_data.

IF bizobj->gt_recs[] IS NOT INITIAL.
IF p_backgr EQ abap_true.
PERFORM background_process.
ELSE.
PERFORM display_data.
ENDIF.

ENDIF. . " FORM-Routines

 
*&---------------------------------------------------------------------*
*& Include ZBM_P_MVC_TEMPLATE_TOP - Report ZBM_P_MVC_TEMPLATE
*& Sample MVC implementation temaplate by BM
*& This include contains global data and selection screen definitions
*&---------------------------------------------------------------------*

REPORT zbm_p_mvc_template.

"Global data
TABLES: zwm_s_lt10_alv, lqua.
DATA: gv_grid TYPE REF TO cl_gui_alv_grid,
bizobj TYPE REF TO zbm_cl_mvc_template.

FIELD-SYMBOLS: <gs_selected_row> TYPE zwm_s_lt10_alv.

SELECTION-SCREEN BEGIN OF BLOCK b1 WITH FRAME TITLE TEXT-t01.

SELECT-OPTIONS: s_lgnum FOR lqua-lgnum OBLIGATORY,
s_lgtyp FOR lqua-lgtyp OBLIGATORY,
s_lgpla FOR lqua-lgpla,
s_matnr FOR lqua-matnr.

PARAMETERS:
p_bwlvs TYPE ltak-bwlvs DEFAULT 999 OBLIGATORY,
p_inglck TYPE xfeld DEFAULT abap_true.

SELECTION-SCREEN END OF BLOCK b1.

SELECTION-SCREEN BEGIN OF BLOCK b2 WITH FRAME TITLE TEXT-t02.

PARAMETERS:
p_backgr TYPE xfeld. "Background processing request flag

SELECTION-SCREEN END OF BLOCK b2.


INITIALIZATION.
bizobj = NEW zbm_cl_mvc_template( ).

 
*&---------------------------------------------------------------------*
*& Include ZBM_P_MVC_TEMPLATE_F01
*& Sample MVC implementation temaplate by BM
*& This include contains Report / ALV related procedures
*& Also acts as controller of MVC implementation
*& by calling related methods of model class.
*&---------------------------------------------------------------------*


******************* Report Related Procedures - Begin **********************

TYPE-POOLS: slis.

"Field catalog, sütunlar için
DATA: gt_fieldcat TYPE slis_t_fieldcat_alv,
gs_fieldcat TYPE LINE OF slis_t_fieldcat_alv,
gt_layout TYPE slis_layout_alv,
g_variant LIKE disvariant.


FORM set_grid_var.
IF gv_grid IS INITIAL.
"Getting the reference to teh ALV grid
CALL FUNCTION 'GET_GLOBALS_FROM_SLVC_FULLSCR'
IMPORTING
e_grid = gv_grid.
ENDIF.
ENDFORM.


FORM prepare_field_catalog.
"Prepare fields catalog
IF gt_fieldcat IS INITIAL.

CALL FUNCTION 'REUSE_ALV_FIELDCATALOG_MERGE'
EXPORTING
i_program_name = sy-repid
i_structure_name = 'zwm_s_lt10_alv'
i_inclname = sy-repid
CHANGING
ct_fieldcat = gt_fieldcat[].

LOOP AT gt_fieldcat ASSIGNING FIELD-SYMBOL(<wa>)
WHERE fieldname EQ 'IS_SELECTED'.
<wa>-checkbox = abap_true.
<wa>-no_out = abap_true.
ENDLOOP.

ENDIF.

ENDFORM.
*&---------------------------------------------------------------------*
*& Form prepare_layout_alv
*&---------------------------------------------------------------------*
FORM prepare_layout_alv .
gt_layout-colwidth_optimize = 'X'.
gt_layout-zebra = 'X'.
gt_layout-no_input = 'X'.
gt_layout-box_fieldname = 'IS_SELECTED'.

ENDFORM.


FORM set_pf_status USING rt_extab TYPE slis_t_extab.
"menüleri ve butonları standart olarak çıkarmayı sağlıyor.
SET PF-STATUS 'STANDARD'.
SET TITLEBAR 'TITLE1' WITH sy-uname.
ENDFORM.

FORM refresh_grid_data.
"Refreshes the grid data
PERFORM set_grid_var.
CALL METHOD gv_grid->refresh_table_display.
ENDFORM.


FORM reload_data.
"Verileri tekrar yükle
PERFORM get_data.
PERFORM refresh_grid_data.
ENDFORM.



"Display data
FORM display_data.
PERFORM prepare_field_catalog.
PERFORM prepare_layout_alv.

IF g_variant IS INITIAL.
g_variant-report = sy-repid.
g_variant-username = sy-uname.
ENDIF.

"Dipslay data
CALL FUNCTION 'REUSE_ALV_GRID_DISPLAY'
EXPORTING
i_callback_program = sy-repid
i_callback_pf_status_set = 'SET_PF_STATUS'
is_layout = gt_layout
it_fieldcat = gt_fieldcat[]
i_default = 'X'
i_save = 'A'
is_variant = g_variant
i_callback_user_command = 'USER_COMMAND'
TABLES
t_outtab = bizobj->gt_recs[].

"Set grid var
PERFORM set_grid_var.
ENDFORM.
******************* Report Related Procedures - End **********************



****************************** MVC Controller Part - Begin **********************

"ALV Komutları
FORM user_command USING r_ucomm
LIKE sy-ucomm rs_selfield TYPE slis_selfield.

CASE r_ucomm.

WHEN '&CREATETO'.
PERFORM create_to.

ENDCASE.

ENDFORM. "USER_COMMAND

"Gets report data
FORM get_data.
"Calls model class to retrieve report data
"That is contoller method, simply passes view parameters to model
"And retrieves data

DATA: r_lgnum TYPE zwm_cl_lt10=>ty_r_lgnum,
r_lgtyp TYPE zwm_cl_lt10=>ty_r_lgtyp,
r_lgpla TYPE zwm_cl_lt10=>ty_r_lgpla,
r_matnr TYPE zwm_cl_lt10=>ty_r_matnr.

MOVE-CORRESPONDING s_lgnum[] TO r_lgnum[].
MOVE-CORRESPONDING s_lgtyp[] TO r_lgtyp[].
MOVE-CORRESPONDING s_lgpla[] TO r_lgpla[].
MOVE-CORRESPONDING s_matnr[] TO r_matnr[].


"Load excel data and display on ALV
DATA(ls_status) = bizobj->get_data(
i_r_lgnum = r_lgnum[]
i_r_lgtyp = r_lgtyp[]
i_r_lgpla = r_lgpla[]
i_r_matnr = r_matnr[]
i_ignore_locked = p_inglck
).
IF ls_Status-status EQ zwm_cl_defs=>c_stat_success.
DATA: lv_cnt TYPE i.
lv_cnt = lines( bizobj->gt_recs[] ).
ENDIF.

MESSAGE ls_status-status_text TYPE 'S' DISPLAY LIKE ls_status-status.

ENDFORM.


FORM create_to.
"Calls model class to Create transfer order in this example
"That is contoller method, simply passes view parameters to model

DATA: lv_ans2 TYPE char1.
CLEAR lv_ans2.
CALL FUNCTION 'POPUP_TO_CONFIRM'
EXPORTING
titlebar = TEXT-tc1
text_question = TEXT-tc2
text_button_1 = TEXT-tc3
icon_button_1 = 'ICON_CHECKED'
text_button_2 = TEXT-tc4
icon_button_2 = 'ICON_CANCEL'
display_cancel_button = ' '
popup_type = 'ICON_MESSAGE_ERROR'
IMPORTING
answer = lv_ans2
EXCEPTIONS
text_not_found = 1
OTHERS = 2.
IF sy-subrc <> 0. ENDIF.

IF lv_ans2 = '2'.
RETURN.
ENDIF.


DATA(ls_state) = bizobj->create_transfer_orders(
i_bwlvs = p_bwlvs
).

PERFORM refresh_grid_data.

MESSAGE ls_state-status_text TYPE 'S' DISPLAY LIKE ls_state-status.
ENDFORM.

FORM background_process.
"Calls model class to Create transfer order in this example
"That is contoller method, simply passes view parameters to model

DATA(ls_state) = bizobj->create_transfer_orders_backg(
i_bwlvs = p_bwlvs
).

MESSAGE ls_state-status_text TYPE 'S' DISPLAY LIKE ls_state-status.
ENDFORM.

****************************** MVC Controller Part - End **********************

 

And model class code is below. Some part of the code is removed.
CLASS zbm_cl_mvc_template DEFINITION
PUBLIC
FINAL
CREATE PUBLIC .

PUBLIC SECTION.

"Types
TYPES: BEGIN OF ty_status,
status TYPE zwm_e_status,
status_text TYPE zwm_e_status_text,
END OF ty_status.

"Range Types
TYPES: ty_R_lgnum TYPE RANGE OF lqua-lgnum,
ty_R_lgtyp TYPE RANGE OF lqua-lgtyp,
ty_R_lgpla TYPE RANGE OF lqua-lgpla,
ty_R_matnr TYPE RANGE OF lqua-matnr.

"Operation return status
CONSTANTS c_stat_success TYPE zwm_e_status VALUE 'S' ##NO_TEXT.
CONSTANTS c_stat_warning TYPE zwm_e_status VALUE 'W' ##NO_TEXT.
CONSTANTS c_stat_error TYPE zwm_e_status VALUE 'E' ##NO_TEXT.
CONSTANTS c_stat_info TYPE zwm_e_status VALUE 'I' ##NO_TEXT.


DATA: gt_recs TYPE zwm_ty_s_lt10_alv.

METHODS get_data
IMPORTING
VALUE(i_r_lgnum) TYPE ty_r_lgnum
VALUE(i_r_lgtyp) TYPE ty_r_lgtyp
VALUE(i_r_lgpla) TYPE ty_r_lgpla
VALUE(i_r_matnr) TYPE ty_r_matnr
VALUE(i_ignore_locked) TYPE xfeld OPTIONAL
RETURNING VALUE(e_status) TYPE ty_status.

METHODS create_transfer_orders
IMPORTING
i_bwlvs TYPE ltak-bwlvs OPTIONAL
RETURNING VALUE(e_status) TYPE ty_status.

METHODS create_transfer_orders_backg
IMPORTING
i_bwlvs TYPE ltak-bwlvs OPTIONAL
RETURNING VALUE(e_status) TYPE ty_status.

PROTECTED SECTION.
PRIVATE SECTION.

METHODS set_status
RETURNING VALUE(e_status) TYPE ty_Status.

METHODS create_transfer_order
IMPORTING
i_bwlvs TYPE ltak-bwlvs OPTIONAL
CHANGING c_rec TYPE zwm_s_lt10_alv
RETURNING VALUE(e_status) TYPE ty_Status.


ENDCLASS.



CLASS ZBM_CL_MVC_TEMPLATE IMPLEMENTATION.


* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Instance Private Method ZBM_CL_MVC_TEMPLATE->CREATE_TRANSFER_ORDER
* +-------------------------------------------------------------------------------------------------+
* | [--->] I_BWLVS TYPE LTAK-BWLVS(optional)
* | [<-->] C_REC TYPE ZWM_S_LT10_ALV
* | [<-()] E_STATUS TYPE TY_STATUS
* +--------------------------------------------------------------------------------------</SIGNATURE>
METHOD create_transfer_order.
DATA: exref TYPE REF TO cx_root.
TRY.

DATA: lt_return TYPE TABLE OF bapiret2.
CALL FUNCTION 'L_TO_CREATE_SINGLE'
EXPORTING
i_bwlvs .....

IF c_rec-status_text IS INITIAL.
MESSAGE s024(zwm) WITH c_rec-lgnum c_rec-tanum INTO c_rec-status_text.
ENDIF.

ENDIF.
CATCH cx_root INTO exref.
c_rec-status = zwm_cl_defs=>c_stat_error.
c_rec-status_text = exref->get_text( ).
ENDTRY.
ENDMETHOD. "create_transfer_order


* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Instance Public Method ZBM_CL_MVC_TEMPLATE->CREATE_TRANSFER_ORDERS
* +-------------------------------------------------------------------------------------------------+
* | [--->] I_BWLVS TYPE LTAK-BWLVS(optional)
* | [<-()] E_STATUS TYPE TY_STATUS
* +--------------------------------------------------------------------------------------</SIGNATURE>
METHOD create_transfer_orders.
DATA: exref TYPE REF TO cx_root.
TRY.

DATA lv_errored_recs TYPE i.
LOOP AT gt_recs ASSIGNING FIELD-SYMBOL(<fs_rec>)
WHERE is_selected EQ abap_true
AND is_locked IS INITIAL.

create_transfer_order(
EXPORTING
i_bwlvs = i_bwlvs
CHANGING
c_rec = <fs_rec>
).

"Check all controls
IF <fs_rec>-status NE c_stat_success.
Lv_errored_recs = Lv_errored_recs + 1.
ENDIF.

ENDLOOP.
IF sy-subrc IS INITIAL.
IF lv_errored_recs EQ 0.
e_status-status = zwm_cl_defs=>c_stat_success.
MESSAGE s029(zwm) INTO e_status-status_text.
ELSE.
e_status-status = zwm_cl_defs=>c_stat_warning.
MESSAGE s014(zwm) WITH lv_errored_recs INTO e_status-status_text.
ENDIF.
ELSE.
e_status-status = zwm_cl_defs=>c_stat_warning.
MESSAGE s026(zwm) INTO e_status-status_text.
ENDIF.

CATCH cx_root INTO exref.
e_status-status = c_stat_error.
e_status-status_text = exref->get_text( ).
ENDTRY.

ENDMETHOD.


* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Instance Public Method ZBM_CL_MVC_TEMPLATE->CREATE_TRANSFER_ORDERS_BACKG
* +-------------------------------------------------------------------------------------------------+
* | [--->] I_BWLVS TYPE LTAK-BWLVS(optional)
* | [<-()] E_STATUS TYPE TY_STATUS
* +--------------------------------------------------------------------------------------</SIGNATURE>
METHOD create_transfer_orders_backg.
DATA: exref TYPE REF TO cx_root.
TRY.

LOOP AT gt_recs ASSIGNING FIELD-SYMBOL(<wa>).
<wa>-is_selected = abap_true.
ENDLOOP.

create_transfer_orders(
i_bwlvs = i_bwlvs
).

CATCH cx_root INTO exref.
e_status-status = c_stat_error.
e_status-status_text = exref->get_text( ).
ENDTRY.
ENDMETHOD.


* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Instance Public Method ZBM_CL_MVC_TEMPLATE->GET_DATA
* +-------------------------------------------------------------------------------------------------+
* | [--->] I_R_LGNUM TYPE TY_R_LGNUM
* | [--->] I_R_LGTYP TYPE TY_R_LGTYP
* | [--->] I_R_LGPLA TYPE TY_R_LGPLA
* | [--->] I_R_MATNR TYPE TY_R_MATNR
* | [--->] I_IGNORE_LOCKED TYPE XFELD(optional)
* | [<-()] E_STATUS TYPE TY_STATUS
* +--------------------------------------------------------------------------------------</SIGNATURE>
METHOD get_data.
DATA: exref TYPE REF TO cx_root.
TRY.
"Get pending records of LT10
SELECT *
INTO CORRESPONDING FIELDS OF TABLE gt_recs
FROM lqua ....

IF gt_recs IS NOT INITIAL.
e_status-status = c_stat_success.
DATA: lv_cnt TYPE i.
lv_cnt = lines( gt_recs ).
MESSAGE s027(zwm) WITH lv_cnt INTO e_status-status_text.
ELSE.
e_status-status = c_stat_warning.
MESSAGE s028(zwm) INTO e_status-status_text.
ENDIF.

CATCH cx_root INTO exref.
e_status-status = c_stat_error.
e_status-status_text = exref->get_text( ).
ENDTRY.

ENDMETHOD.


* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Instance Private Method ZBM_CL_MVC_TEMPLATE->SET_STATUS
* +-------------------------------------------------------------------------------------------------+
* | [<-()] E_STATUS TYPE TY_STATUS
* +--------------------------------------------------------------------------------------</SIGNATURE>
METHOD set_status.
DATA: exref TYPE REF TO cx_root.
TRY.

IF gt_recs IS NOT INITIAL.

SELECT .......
ENDIF.

e_status-status = c_stat_success.

CATCH cx_root INTO exref.
e_status-status = c_stat_error.
e_status-status_text = exref->get_text( ).
ENDTRY.
ENDMETHOD.
ENDCLASS.

 

Sample ITS Dialog Screen program;

Here is a program with three dialog screens. I will share screens 1001's and 1002's codes and layouts. At the 1001 screen user enters a transfer order and all the data is processed in model class or calls 1003 screen to pick a transfer order from list.


 


1001 Screen Layout and Attributes



*&---------------------------------------------------------------------*
*& Report ZSD_P_PRNT_SHP_LBL
*&---------------------------------------------------------------------*
*&
*&---------------------------------------------------------------------*

INCLUDE zsd_p_prnt_shp_lbl_top. " Global Data
INCLUDE zsd_p_prnt_shp_lbl_f1o01. "Screen 1001 codes
INCLUDE zsd_p_prnt_shp_lbl_f1o02. "Screen 1002 codes
INCLUDE zsd_p_prnt_shp_lbl_f1o03. "Screen 1003 codes

 

As you can see, there is no global data except object itself. All the screen related fields are stored in obj.
*&---------------------------------------------------------------------*
*& Include ZSD_P_PRNT_SHP_LBL_TOP - Report ZSD_P_PRNT_SHP_LBL
*&---------------------------------------------------------------------*
REPORT ZSD_P_PRNT_SHP_LBL.

"obj's public data is used directly on screen fields too
DATA: obj TYPE REF TO zsd_cl_p_prnt_shp_lbl_bo.

 

1001 Screens's code.
*----------------------------------------------------------------------*
***INCLUDE ZSD_P_PRNT_SHP_LBL_F1O01.
*----------------------------------------------------------------------*
*&---------------------------------------------------------------------*
*& Module STATUS_1001 OUTPUT
*&---------------------------------------------------------------------*
*&
*&---------------------------------------------------------------------*
MODULE status_1001 OUTPUT.
PERFORM status_1001.
ENDMODULE.
*&---------------------------------------------------------------------*
*& Module USER_COMMAND_1001 INPUT
*&---------------------------------------------------------------------*
* text
*----------------------------------------------------------------------*
MODULE user_command_1001 INPUT.
PERFORM 1001_command.
ENDMODULE.

FORM status_1001.
SET PF-STATUS 'ST_NOMENU'.
SET TITLEBAR 'T_1001'.

IF obj IS INITIAL.
obj = NEW zsd_cl_p_prnt_shp_lbl_bo( ).
ENDIF.

"Set Cursor Field
IF obj->scr_1001-tanum IS INITIAL.
SET CURSOR FIELD 'obj->scr_1001-tanum'.
ENDIF.

PERFORM process_call_back_1001.

ENDFORM.

FORM 1001_command.
"1001 ekranı komutları

DATA: lv_ucomm TYPE sy-ucomm.
lv_ucomm = sy-ucomm.
CLEAR sy-ucomm.

CASE lv_ucomm.
WHEN 'NEXT' OR ''.
PERFORM next_1001.

WHEN 'CLR'.
PERFORM clear_1001.

WHEN 'BACK'.
LEAVE TO SCREEN 0.

WHEN 'F4ORD'.
PERFORM call_1003.

ENDCASE.

ENDFORM.

FORM next_1001.
DATA(res) = obj->process_next_1001( ).

IF res-status EQ obj->c_status_success.
PERFORM call_1002.

ELSEIF res-status EQ obj->c_status_confirm.
PERFORM confirm_1001 USING res-status_text.

ELSE.
PERFORM clear_1001.

CALL FUNCTION 'ZWM_FM_CALL_MSG_SCR_TEXT'
EXPORTING
i_msg = res-status_text.
ENDIF.
ENDFORM.

FORM clear_1001.
obj->clear_1001( ).
ENDFORM.

FORM confirm_1001 USING stat_text.
DATA answer TYPE char1.
CALL FUNCTION 'ZWM_FM_CALL_CONF_SCR_TEXT'
EXPORTING
i_msg = stat_text
IMPORTING
answer = answer.

IF answer NE 'X'.
PERFORM clear_1001.
ELSE.
PERFORM call_1002.
ENDIF.
ENDFORM.

FORM call_1002.

DATA(res) = obj->before_calling_1002( ).
IF res-status NE obj->c_status_success.
CALL FUNCTION 'ZWM_FM_CALL_MSG_SCR_TEXT'
EXPORTING
i_msg = res-status_text.
ENDIF.

LEAVE TO SCREEN '1002'.

ENDFORM.


FORM call_1003.

DATA(res) = obj->before_calling_1003( ).
IF res-status EQ obj->c_status_success.
obj->scr_1003-callbackscr = sy-dynnr.
LEAVE TO SCREEN '1003'.
ELSE.
CALL FUNCTION 'ZWM_FM_CALL_MSG_SCR_TEXT'
EXPORTING
i_msg = res-status_text.
ENDIF.



ENDFORM.

FORM process_call_back_1001.

IF obj->scr_1003-callbackscr EQ sy-dynnr AND obj->scr_1003-sel_rec-tanum IS NOT INITIAL.
obj->scr_1001-tanum = obj->scr_1003-sel_rec-tanum.
CLEAR obj->scr_1003-callbackscr.
PERFORM next_1001.
ENDIF.

ENDFORM.

 

1003 Screen and Code

1003 screen is called by 1001 screen when "Search Transfer Order" button is clicked.


1003 Screen Layout and Elements


Code of 1003
*&---------------------------------------------------------------------*
*& Include ZWM_P_MOBILE_SCREEN_1003
*&---------------------------------------------------------------------*
*&---------------------------------------------------------------------*
*& Module STATUS_1003 OUTPUT
*&---------------------------------------------------------------------*
*& That is the detail screen of 1003, please call 1001 first
*&---------------------------------------------------------------------*
MODULE status_1003 OUTPUT.
PERFORM status_1003.
ENDMODULE.
*&---------------------------------------------------------------------*
*& Module USER_COMMAND_1003 INPUT
*&---------------------------------------------------------------------*
* text
*----------------------------------------------------------------------*
MODULE user_command_1003 INPUT.
PERFORM 1003_command.
ENDMODULE.

FORM status_1003.
SET PF-STATUS 'ST_NOMENU'.
SET TITLEBAR 'T_1003'.

IF obj IS INITIAL.
LEAVE TO SCREEN '1001'.
ENDIF.

ENDFORM.

FORM 1003_command.

DATA: lv_ucomm TYPE sy-ucomm.
lv_ucomm = sy-ucomm.
CLEAR sy-ucomm.

IF lv_ucomm EQ 'BACK'.
CLEAR obj->scr_1003-sel_rec.
LEAVE TO SCREEN obj->scr_1003-callbackscr.

ELSEIF lv_ucomm EQ 'NEXT'.
LEAVE TO SCREEN obj->scr_1003-callbackscr.


ELSEIF lv_ucomm EQ 'FORW'.
PERFORM list_next_1003.

ELSEIF lv_ucomm EQ 'PREV'.
PERFORM list_prev_1003.

ENDIF.

ENDFORM.

FORM list_next_1003.
DATA: lv_lines TYPE i.
DESCRIBE TABLE obj->scr_1003-transfer_orders LINES obj->scr_1003-lines.

lv_lines = lv_lines - 1.
IF obj->scr_1003-line LT obj->scr_1003-lines .
obj->scr_1003-line = obj->scr_1003-line + 1.
ENDIF.

ENDFORM.

FORM list_prev_1003.

IF obj->scr_1003-line NE 0.
obj->scr_1003-line = obj->scr_1003-line - 1.
ENDIF.

ENDFORM.

FORM back_1003.
"obj->clear_1003( ).
LEAVE TO SCREEN '1003'.
ENDFORM.

*&---------------------------------------------------------------------*
*& Module FILL_1003_TAB OUTPUT
*&---------------------------------------------------------------------*
*&
*&---------------------------------------------------------------------*
MODULE fill_1003_tab OUTPUT.
PERFORM fill_tab_line_1003.
ENDMODULE.
*&---------------------------------------------------------------------*
*& Module READ_1003_TAB INPUT
*&---------------------------------------------------------------------*
* text
*----------------------------------------------------------------------*
MODULE read_1003_tab INPUT.
PERFORM read_tab_1003.
ENDMODULE.


FORM read_tab_1003.
"Set line range and load screen table
obj->scr_1003-lines = sy-loopc.
obj->scr_1003-idx = sy-stepl + obj->scr_1003-line.

IF obj->scr_1003-line_data-checked EQ abap_true.
obj->scr_1003-sel_rec = obj->scr_1003-line_data.
ENDIF.

ENDFORM.

FORM fill_tab_line_1003.
obj->scr_1003-idx = sy-stepl + obj->scr_1003-line.
READ TABLE obj->scr_1003-transfer_orders INTO obj->scr_1003-line_data
INDEX obj->scr_1003-idx.
ENDFORM.

 

 

And class code, specific to this dialog screens' program.
CLASS zsd_cl_p_prnt_shp_lbl_bo DEFINITION
PUBLIC
INHERITING FROM zsd_cl_p_prnt_shp_lbl_base
FINAL
CREATE PUBLIC .

PUBLIC SECTION.

TYPES:
BEGIN OF gty_scr_1001,
tanum TYPE ltak-tanum,
END OF gty_scr_1001 .

TYPES:
BEGIN OF gty_scr_1003,
callbackscr TYPE sy-dynnr,
transfer_orders TYPE zwm_ty_s_to_with_delv,
sel_rec TYPE LINE OF zwm_ty_s_to_with_delv,

"Screen table indexes
idx TYPE syst_stepl,
line TYPE int4,
lines TYPE int4,

line_data TYPE LINE OF zwm_ty_s_to_with_delv,

END OF gty_scr_1003 .

DATA lbldata TYPE gty_lbldata .
DATA print_settigs TYPE gty_print_settings .
DATA scr_1001 TYPE gty_scr_1001 . "Stores all data related to screen 1001
DATA scr_1003 TYPE gty_scr_1003 . "Stores all data related to screen 1003

METHODS process_next_1001
RETURNING
VALUE(result) TYPE gty_status .

METHODS clear_1001 .

METHODS before_calling_1002
RETURNING
VALUE(result) TYPE gty_status .

METHODS print_1002
RETURNING
VALUE(result) TYPE gty_status .

METHODS before_calling_1003
RETURNING
VALUE(result) TYPE gty_status .

PROTECTED SECTION.

PRIVATE SECTION.

CONSTANTS: c_doc_type_delv TYPE vbfa-vbtyp_v VALUE 'J',
c_delv_stat_picked TYPE likp-kostk VALUE 'C',
c_to_confirmed TYPE ltak-kquit VALUE 'X'.

METHODS fill_label_data_via_to
IMPORTING
VALUE(uname) TYPE sy-uname OPTIONAL
VALUE(tanum) TYPE ltak-tanum
RETURNING
VALUE(result) TYPE gty_status .

METHODS fill_print_settings_data
RETURNING
VALUE(result) TYPE gty_status .
......

ENDCLASS.



CLASS ZSD_CL_P_PRNT_SHP_LBL_BO IMPLEMENTATION.


* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Instance Public Method ZSD_CL_P_PRNT_SHP_LBL_BO->BEFORE_CALLING_1002
* +-------------------------------------------------------------------------------------------------+
* | [<-()] RESULT TYPE GTY_STATUS
* +--------------------------------------------------------------------------------------</SIGNATURE>
METHOD before_calling_1002.
DATA: exref TYPE REF TO cx_root.
TRY.

result = get_cust_info( ).
IF result-status EQ c_status_success.
result = fill_print_settings_data( ).
ENDIF.

CATCH cx_root INTO exref.
result-status = c_status_error.
result-status_text = exref->get_text( ).
ENDTRY.
ENDMETHOD.


* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Instance Public Method ZSD_CL_P_PRNT_SHP_LBL_BO->CLEAR_1001
* +-------------------------------------------------------------------------------------------------+
* +--------------------------------------------------------------------------------------</SIGNATURE>
METHOD clear_1001.
CLEAR: lbldata ,scr_1001.
ENDMETHOD.


* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Instance Private Method ZSD_CL_P_PRNT_SHP_LBL_BO->FILL_LABEL_DATA_VIA_TO
* +-------------------------------------------------------------------------------------------------+
* | [--->] UNAME TYPE SY-UNAME(optional)
* | [--->] TANUM TYPE LTAK-TANUM
* | [<-()] RESULT TYPE GTY_STATUS
* +--------------------------------------------------------------------------------------</SIGNATURE>
METHOD fill_label_data_via_to.
DATA: exref TYPE REF TO cx_root.
TRY.

CLEAR lbldata.
lbldata-uname = uname.
IF lbldata-uname IS INITIAL.
lbldata-uname = sy-uname.
ENDIF.

lbldata-to-tanum = tanum.

result = get_to_and_validate( ).
IF result-status EQ c_status_success.
result = get_delv_and_validate( ).
ENDIF.

CATCH cx_root INTO exref.
result-status = c_status_error.
result-status_text = exref->get_text( ).
ENDTRY.
ENDMETHOD.

***************

ENDCLASS.

 

As you can see in that dialog screen example, you can create many layers as you need. As long as you do not directly write your code under view object itself, you increase modularity and code reusability. Testing becomes easier. You can test class methods directly. Provides many benefits.

So these were basic examples of ,how to write an abap report by applying MVC pattern. MVC enables us to split view and business logic code. We can re-use model class for different types of interfaces. Within Fiori projects, frontend and backend development can be split and MVC enables us to share tasks. One can write backend and other can write front end.

Depending on interest, I can add more mvc templates. The logic is same, you can apply mvc to any code.

And if you like the idea of lego like ,reusable and robust coding, you must check solid rules too ,which is very important.  Here is a link for solid. And a link for clean abap.

I hope that gives some of you an idea about MVC layering in Abap. Please feel free to add your own examples in comments.

Thanks for reading.

 

Edit(27.10.2022): A sample on how to apply mvc on Adobe/Smartforms -> Blog Post.
13 Comments
Looks great. our project also has some reports, we usually create a big class as business class and put all logic into it(even display), the report just provide the parameters and do some checks, what do you think about this way?
beyhan_meyrali
Active Contributor
0 Kudos
Hi Wenjie,

MVC is one of the patterns to write better code. I think you also need to pay attention to solid rules. One big class, too many things inside, that is probably against solid.

And regarding a class, even with display methods; you can create many sub layers in MVC, view layer may have many sub layers, same is true for model as well. That is just fine. But, according your description, you have created a layer/class for view and that layer also contains biz logic in it. That is partially okay but not right. Instead, create another class or classes and keep biz logic in those classes. Why? Ask yourself, will I use those display methods if I need to create a new view, such as rfc or web api or print output, can I use same methods in those views too? That will give you the answer.  That is also part of solid scope.

Thanks for comment and question.
matt
Active Contributor
0 Kudos

It goes against many principles of good software design. Far better to have many classes and interfaces, with small methods. This also makes it easier to write to ABAP Unit tests, which will help you identify if a new change has broken something.

I suggest you research SOLID - https://en.wikipedia.org/wiki/SOLID

 

matt
Active Contributor
MVC is a useful pattern so it's good you wrote about it. However, I find it odd that you're using FORMs in your report., since these are essentially obsolete in new developments. Better to use a local class for your screen handling.

Furthermore, it would be better to use:

  • An interface for the public methods to the business logic

  • A concrete class implementing that interface

  • A factory class to return a concrete instance of your interface


This is the pattern I use for (nearly) all global classes, for three reasons.

  1. It enables greater flexibility when enhancments come along

  2. It makes the application as a whole more robust

  3. An interface means it's easy to write test doubles when writing ABAP unit tests.


 
beyhan_meyrali
Active Contributor
0 Kudos
It will be good if you can share a template or place a reference so people can have a look or even copy and use that template if they prefer to use local classes.
matt
Active Contributor

You just convert your FORMs into methods of lcl_main.

So partial example.

PROGRAM.

CLASS lcl_main DEFINITION.
PUBLIC SECTION.
METHODS:
go.
PRIVATE SECTION.
DATA bizobj TYPE REF TO z...
METHODS:
background_process,
display_data,
get_data.
ENDCLASS.

CLASS lcl_main IMPLEMENTATION.
METHOD go.
get_data( ).
IF bizobj->has_records( ).
IF p_backgr EQ abap_true.
background_process( ).
ELSE.
display_data( ).
ENDIF.
ENDIF.
ENDMETHOD.

METHOD background_process.
...
ENDMETHOD.

METHOD display_data.
...
ENDMETHOD.

METHOD get_data.
"Calls model class to retrieve report data
"That is contoller method, simply passes view parameters to model
"And retrieves data

DATA: r_lgnum TYPE zwm_cl_lt10=>ty_r_lgnum,
r_lgtyp TYPE zwm_cl_lt10=>ty_r_lgtyp,
r_lgpla TYPE zwm_cl_lt10=>ty_r_lgpla,
r_matnr TYPE zwm_cl_lt10=>ty_r_matnr.

MOVE-CORRESPONDING s_lgnum[] TO r_lgnum.
MOVE-CORRESPONDING s_lgtyp[] TO r_lgtyp.
MOVE-CORRESPONDING s_lgpla[] TO r_lgpla.
MOVE-CORRESPONDING s_matnr[] TO r_matnr.


"Load excel data and display on ALV
DATA(ls_status) = bizobj->get_data(
i_r_lgnum = r_lgnum
i_r_lgtyp = r_lgtyp
i_r_lgpla = r_lgpla
i_r_matnr = r_matnr
i_ignore_locked = p_inglck
).
IF ls_Status-status EQ zwm_cl_defs=>c_stat_success.
DATA(lv_cnt) = bizobj->get_number_of_records( ).
ENDIF.

MESSAGE ls_status-status_text TYPE 'S' DISPLAY LIKE ls_status-status.
ENDMETHOD.
ENDCLASS.

DATA main TYPE REF TO lcl_main.

INITALIZATION.
main = new #( ).

END-OF-SELECTION.
main->go( ).

Note I've changed your access to bizobj->gt_recs[]. It's bad practice to give outside objects read and write access to the classes attributes. Ideally, they should be defined only in the private section. Sometimes it's ok for public section, but with READ-ONLY. Generally access to the class's attribute should ONLY be through methods.

Also, I've removed the [] table identifiers. You don't need them, except for select options.

A few other points.
TABLES is obsolete. Instead of

TABLES tabname.
SELECT-OPTIONS s_field FOR tabname-field.

use

DATA g_field TYPE tabname-field.
SELECT-OPTIONS s_field FOR g_field.

Check whether you need the TYPE-POOLS statements. Many are now automatically included into all ABAP programs.

I'd shift everything that controls the ALV into a separate class. So

  • REPORT with the select options and screen is the view.
  • Another class handles the ALV - this may be the controller, but it may sit between the view and the controller.
  • A model class for the business logic.

Finally for variable prefixes. According to the SAP style guide (and the ABAP Clean Code books, and DSUG), you should avoid them.

 

 

beyhan_meyrali
Active Contributor
0 Kudos
Very good example and thanks for commenting. I would just suggest avoiding local classes. And regarding using forms and avoiding encapsulation, sometimes you need to make compromises. It might be necessary to create a good a framework with interfaces and classes, but sometimes not. Sometimes it is better to keep simple yet modular. I try to avoid over complexity.
matt
Active Contributor

Why avoid local classes? They're simple and easy to understand. You can use OO design techniques much earlier than FORMs.

It's the only alternative to using FORMs and as I've said, FORMs are obsolete. (Although FORMs are a form encapsulation).

Also, if you object to local classes - you'll find writing ABAP unit tests tremendously difficult! Do you avoid them because you can't use SE24? If you code in ADT Eclipse, you'll be used to not relying on that.

Here's another report I did earlier.

REPORT.
DATA okcode TYPE okcode.

DATA: g_bukrs TYPE bkpf-bukrs,
g_belnr TYPE bkpf-belnr,
g_gjahr TYPE bkpf-gjahr,
g_blart TYPE bkpf-blart,

g_vbeln TYPE vbak-vbeln,
g_vkorg TYPE vbak-vkorg,

g_aufnr TYPE aufk-aufnr,
g_werks TYPE aufk-werks,

g_auart TYPE vbak-auart.

PARAMETERS: r_findoc RADIOBUTTON GROUP rep DEFAULT 'X' USER-COMMAND rad, " Financial documents
r_sd RADIOBUTTON GROUP rep, " SD contracts
r_cs_ord RADIOBUTTON GROUP rep. " CS orders

SELECTION-SCREEN SKIP.

SELECT-OPTIONS: s_bukrs FOR g_bukrs MATCHCODE OBJECT dbukn MODIF ID fi, " Company code
s_belnr FOR g_belnr MATCHCODE OBJECT fidoc MODIF ID fi, " FI Document number
s_gjahr FOR g_gjahr MODIF ID fi, " Fiscal year
s_blart FOR g_blart MATCHCODE OBJECT f4_blart MODIF ID fi . " FI document type

SELECT-OPTIONS: s_vkorg FOR g_vkorg MATCHCODE OBJECT a_vkorg MODIF ID sd, " Sales org
s_vbeln FOR g_vbeln MODIF ID sd. " SD Document number

SELECT-OPTIONS: s_werks FOR g_werks MATCHCODE OBJECT h_t001w MODIF ID cs, " Plant
s_aufnr FOR g_aufnr MODIF ID cs. " CS Document number

SELECTION-SCREEN BEGIN OF LINE.
SELECTION-SCREEN COMMENT (33) FOR FIELD p_datefr MODIF ID sdc. " Creation Date
PARAMETERS p_datefr TYPE d MODIF ID sdc.
SELECTION-SCREEN POSITION 54.
SELECTION-SCREEN COMMENT (5) TEXT-001 FOR FIELD p_dateto MODIF ID sdc. " to
PARAMETERS p_dateto TYPE d DEFAULT sy-datum MODIF ID sdc.
SELECTION-SCREEN END OF LINE.

SELECT-OPTIONS: s_auart FOR g_auart MATCHCODE OBJECT h_tvak MODIF ID sd, " SD Document type
s_csaua FOR g_auart MATCHCODE OBJECT auart MODIF ID cs. " CD Document type
SELECTION-SCREEN SKIP.
PARAMETERS p_noatt AS CHECKBOX.

CLASS lcl_main DEFINITION.
PUBLIC SECTION.
METHODS:
constructor,
go
IMPORTING
i_report TYPE REF TO zif_document_attachments_rep OPTIONAL,
validate,
open_screen,
process_commands
IMPORTING
i_okcode TYPE okcode,
modify_screen
CHANGING
x_screen TYPE screen,
get_selections
RETURNING
VALUE(r_result) TYPE REF TO zif_ca_selections.
PRIVATE SECTION.
DATA: report TYPE REF TO zif_document_attachments_rep,
selscreen TYPE REF TO zca_doc_attachments_selscreen.
ENDCLASS.

CLASS lcl_main IMPLEMENTATION.

METHOD modify_screen.
CASE abap_true.
WHEN r_findoc.
selscreen->set_fields_for_findocs( CHANGING x_screen = x_screen ).
WHEN r_sd.
selscreen->set_fields_for_sd( CHANGING x_screen = x_screen ).
WHEN r_cs_ord.
selscreen->set_fields_for_cs_orders( CHANGING x_screen = x_screen ).
ENDCASE.
ENDMETHOD.

METHOD go.
report = i_report.
DATA(selections) = get_selections( ).
IF report IS NOT BOUND.
TRY.
report = z_doc_attachments_factory=>get_instance( selections ).
report->go( ).
CATCH zcx_selections INTO DATA(error).
DATA(message) = |Programming error with parameter { error->field }. Contact developer.|.
MESSAGE message TYPE 'E'.
CATCH cx_salv_not_found INTO DATA(salv_error).
MESSAGE salv_error TYPE 'E'.
ENDTRY.
ENDIF.
ENDMETHOD.

METHOD validate.
TRY.
selscreen->set_selections( get_selections( ) ).
selscreen->validate( ).
CATCH zcx_selections INTO DATA(error).
DATA(message) = |Programming error with parameter { error->field }. Contact developer.|.
MESSAGE message TYPE 'E'.
ENDTRY.
ENDMETHOD.

METHOD open_screen.
CALL SCREEN 100.
ENDMETHOD.


METHOD process_commands.
report->process_commands( i_okcode ).
ENDMETHOD.

METHOD constructor.
selscreen = NEW zca_doc_attachments_selscreen( ).
ENDMETHOD.

METHOD get_selections.
r_result = zca_selections_factory=>get_instance( ).
...
" Here the selection screen values are encapsulated into a single instance of a class
ENDMETHOD.

ENDCLASS.

INITIALIZATION.
DATA(g_main) = NEW lcl_main( ).

AT SELECTION-SCREEN OUTPUT.
LOOP AT SCREEN INTO DATA(screen_wa).
g_main->modify_screen( CHANGING x_screen = screen_wa ).
MODIFY SCREEN FROM screen_wa.
ENDLOOP.

AT SELECTION-SCREEN.
g_main->validate( ).

START-OF-SELECTION.
g_main->open_screen( ).

MODULE status_0100 OUTPUT.
g_main->go( ).
ENDMODULE.

MODULE user_command_0100 INPUT.
g_main->process_commands( okcode ).
CLEAR okcode.
ENDMODULE.

You'll see there's very little logic of any kind in the report itself. It's all handled by separate (global) classes, called from the lcl_main class.

guido_s
Participant
0 Kudos

Hi,

concerning the MVC topic there already exists a recommendation by SAP:

See help.sap.com - Encapsulating Classic User Interfaces

Kind regards
Guido

shais
Participant
0 Kudos
Just one comment:

As far as I know, TABLES isn't obsolete.

In my opinion, it is still the best option for defining screen fields with reference to dictionary.

Personally, I recommend to create a separate structure (and use TABLES) for each classic dynpro screen.
matt
Active Contributor
True.

The ABAP documentation says:

Table work areas declared using TABLES are interface work areas and should only be declared in the global declaration part of a program for the following purpose:

  • The statement TABLES is required for exchanging data between dynpro fields and the ABAP program, if the fields were defined in a dynpro in the program by being taken from ABAP Dictionary, . In the dynpro event PBO, the content of the table work area is passed to identically named dynpro fields. In PAI, the system takes the data from identically named dynpro fields.

  • In executable programs, flat table work areas can be used to apply data that is provided for the event GET table_wa from an associated logical databaseTABLES is synonymous with the statement NODES for this purpose.


It's clear though not to use it for select options. So I'll restate:

TABLES is obsolete for defining select options or database operations.

 

 
shais
Participant

In case of SELECT-OPTIONS, it is a case of personal preference.

It might be still more convenient to define one TABLES instead of tens of variables.

If you were to ask me, I would have supported defining SELECT-OPTIONS with TYPE (instead of reference to a variable) in ABAP syntax long long time ago...

Jelena
Active Contributor
I'm slightly annoyed by how the local class needs to be implemented in an ABAP report but dude, as soon as you realize the power of exceptions + propagation, you'll never want to go back to the subroutines.