X3 Hybrid Development

This article describes the specific way of developing X3 business applications called Hybrid development. It can be used from X3 Update 9. You must be skilled in both Classic development and Syracuse development to develop hybrid features.

Before starting it is important to know that the X3 server will evolve and some of the code presented in this article will have to be rewritten. This means that any hybrid development you do will have to be refactored for future releases. Please read carefully the caution chapter, it contains important information.

Table of Contents

  1. Presentation
    1. What is X3 hybrid development?
    2. When and why should I use hybrid development?
    3. What are the disadvantages?
    4. Caution
  2. Let's develop some great hybrid code!
    1. Class development
    2. Classic UI
    3. Linking Classic UI and classes
    4. Header / lines management
    5. Miscelleanous
  3. Real Life Example

Presentation

What is X3 hybrid development?

Hybrid development is a Syracuse class development combined with a Classic UI development. Syracuse classes are embedded into Classic UI windows and screens (also called masks in X3). The Classic scripts attached to the screens manage exchanges between screens and classes (i.e. between screen fields and class properties) and invoke CRUD methods.

Classic windows and screens should be used in page mode and not in field mode as much as possible (no field actions if possible) because field action code will be lost when Representations will replace Classic windows.

As usual classes must guarantee the consistency of data inserted or updated in the database.

Hybrid principle

When shoud I use hybrid development?

You have new business objects to develop or you want to start SOA transformation of existing X3 objects. Use hybrid development instead of full Syracuse development for the following reasons:

The main benefit is to start developing using the Syracuse technology, even if some UI behaviors or services, mandatory for you, are not yet available in Syracuse. It avoids postponing usage of Syracuse or SOA transformation, letting teams continue to use Classic development. All work done to develop classes is an investment for the future. Classes will be directly re-usable with Representations or as Services.

Do not opposite Hybrid development and Full Syracuse development

In both cases it is a Syracuse development using classes. Adding a Classic UI is just an option you can take if needed, and you can have a Classic UI and a Syracuse UI if relevant on the same class. For example you may want a Classic UI on the desktop and a mobile representations to access the class through a mobile device.

What are the disadvantages?

The main disadvantage is that developing hybrid features means you are not developing in full Syracuse. However, it is better to develop hybrid features rather than Classic ones.

All code written to manage the Classic part will be lost. So, as much as possible, write less Classic code. Fortunately, the heart of the feature, the business part, is coded in the class and will not be lost.

Caution

Combined objects

Hybrid development is usable for objects with a header table and tables for collections. It doesn't work for "combined" objets, i.e. objects managed as header/lines objets but in a single table.

If you are developing a new feature you should use a data model with a table for the header and tables for collections because this model perfectly fits with Syracuse.

Etna server

Note, Hybrid development is a mix between Classic development and Syracuse development. Currently a single server is running both Classic and Syracuse code. In the future a new server called ETNA - development in progress - dedicated to run Syracuse code will be launched. Once ETNA is available interactions between Classic code and Syracuse code will be done through a dedicated API, so existing Classic code related to managing classes will need to be rewritten.

Learn more about ETNA and Classic code interoperability.

Let's develop some great hybrid code!

Step 1. Class development

The Class development must follow all rules of regular Syracuse development. It must be service oriented and the Class must handle all consistency controls and all business logic.

When the development is done your unit tests should be able to control the behavior of the Class for CRUD operations and Methods. Unit tests test class business logic, not representations.

Step 2. Defining Classic UI

We have to define Metada in X3 dictionaries:

Do not develop code yet in the SUBxxx script of your screens.

Step 3. Linking Classic UI and classes

Once you have your Classic metadata and your class, you have to create a bi-directionnal link between them. It simply means that you have to write in your SUBxxx script the code to manage exchanges between the screens and the class.

Links

You will find below what is important to add in the SUBxxx to manage exchanges with your class. Obviously only code useful for the hybrid management is presented, you can add code and use other Classic object actions depending on your needs. This first overview is completed by a header/lines management dedicated section.

Common actions

OUVRE

In this action you have to set the Supervisor ANOWRITE variable to 1 to disable the regular Classic CUD operations for the whole function (they will be handled by the class itself). Note that I wrote CUD and not CRUD, because you will still use the Classic Read operation as explained in the Read actions chapter.

ANOWRITE = 1

You may also want to open relevant tables and to declare working variables, such as an instance for your class. Please choose a meaningful and unique name for your global variable because your script may call or be called by another script through the tunnel mechanism.

Global Instance MYINSTANCE Using C_MYCLASS

Note that the instance is Global and not Local because the deletion management in the Supervisor is done through a subprogram and the instance must be known in it.

FERME

When you are leaving the transaction, you have to free the instance.

If (MYINSTANCE <> null) : FreeGroup MYINSTANCE : EndifKill MYINSTANCE

Read actions

The reading part is an exception in the hybrid development principles, because you will let the Supervisor read the database and directly feed screen fields instead of using the class. It is easier for the developer, as there is no need to guarantee data integrity because you are reading and not updating data.

LIENS

The Supervisor has loaded the screens from the main table, you just have to instantiate and to load the class to have it online for update and delete operations.

If (MYINSTANCE <> null) : FreeGroup MYINSTANCE : endifMYINSTANCE = NewInstance C_MYCLASS AllocGroup nullLocal Integer MY_STATUS[L]MY_STATUS = fmet MYINSTANCE.AREAD([M:SCREEN]KEY)If ([L]MY_STATUS >= [V]CST_AERROR)# Error managementGMESSAGE = <a function to retrieve the most severe message error in the error stack will be available soon>GOK = 0 : GERR = 1ReturnEndif

Creation actions

You are in the creation process. Because you are developing a hybrid feature you will replace the Classic creation by the Syracuse creation.
To create an object you have to transfer screen field values into class properties, to invoke the create method, and to refresh screen fields.

RAZCRE

You create a new instance of the class. You may have a class already instantiate for another object, so you start by freeing this instance.

If (MYINSTANCE <> null) : FreeGroup MYINSTANCE : endifMYINSTANCE = NewInstance C_MYCLASS AllocGroup null

Then you have to initialize the class properties by calling the AINIT method and/or setting properties manually

Local Integer MY_STATUS[L]MY_STATUS = fmet MYINSTANCE.AINIT()If ([L]MY_STATUS >= [V]CST_AERROR)GMESSAGE = <a function to retrieve the most severe message error in the error stack will be available soon>GOK = 0 : GERR = 1ReturnEndif

To finish you have to refresh the UI to display the values of initialized properties. Complete the process for properties and fields with non matching name

SetInstance [M:SCREEN] With MYINSTANCE[M:SCREEN]FIELD = MYINSTANCE.PROPERTYAffzo [M:SCREEN]1-99
CREATION

You have to set properties from the [F] file (the [F] file has been updated by the Supervisor using values in screens before the call to CREATION) and/or from [M] screens.

You can use the instruction SetInstance which will set propertie's values from field's values for propertie's names that are identical to a field's name of the [F] file.

SetInstance MYINSTANCE With [F:FILE]

You may complete the SetInstance if needed with data from screens or file (if names are not matching) and for translatable texts (AXX type) because they are not in the table.

MYINSTANCE.PROPERTYA = [M:SCREEN]FIELD1MYINSTANCE.PROPERTYB = [F:FILE]FIELD2

Properties have been set, it is time to invoke the creation method of the class.

Local Integer MY_STATUS[L]MY_STATUS = fmet MYINSTANCE.AINSERT()If ([L]MY_STATUS >= [V]CST_AERROR)# Error managementGMESSAGE = <a function to retrieve the most severe message error in the error stack will be available soon>GOK = 0 : GERR = 1ReturnEndif

After a successful insert, refresh the [F] file because the key may be generated in the AINSERT method.

SetInstance [F:FILE] With MYINSTANCE

Don't forget to manage errors during the insert and to set GMESSAGE, GOK and GERR as shown to retrieve the error in the Classic UI.

APRES_CRE

This action is called after a successful creation. You may have to refresh some screen fields, because some information may have been set or updated during the AINSERT method. An easy way to do that is to call the label RELIT from the GOBJSUB script, which calls the LIENS action of our script

Gosub RELIT From GOBJSUB

Update actions

The update process is quite similar to the create process: you have to transfer screen field values into class properties, to invoke the update method, and to refresh screen fields. But because the object already exist they are some differences.

INIMOD

You don't have to instantiate your class in this action: it has already be done during the reading part ($LIENS).

MODIF

As for the creation you have to set properties from the [F] file (the [F] file has been updated by the Supervisor using values in screens before the call to MODIF) and/or from [M] screens.

SetInstance MYINSTANCE With [F:FILE]

You may complete the SetInstance if needed with data from screens or file (if names are not matching) and for translatable texts (AXX type) because they are not in the table.

MYINSTANCE.PROPERTYA = [M:SCREEN]FIELD1MYINSTANCE.PROPERTYB = [F:FILE]FIELD2

It is now time to invoke the update method.

Local Integer MY_STATUS[L]MY_STATUS = fmet MYINSTANCE.AUPDATE()If ([L]MY_STATUS >= [V]CST_AERROR)# Error managementGMESSAGE = <a function to retrieve the most severe message error in the error stack will be available soon>GOK = 0 : GERR = 1ReturnEndif
APRES_MOD

This action is called after a successful update. You may have to refresh some screen fields, because some information may have been set or updated during the AUPDATE method. An easy way to do that is to call the label RELIT from the GOBJSUB script, which calls the LIENS action of our script

Gosub RELIT From GOBJSUB

Duplication actions

The duplication is enabled by default. I remind you that to forbid the duplication you have to add the code Call VIREBOUT (CHAINE,"D") From GOBJET in the $SETBOUT label.

RAZDUP

This action is called when a field of the primary key is changed. You have to free the current instance of you class, create a new one and init it.

If (MYINSTANCE <> null) : FreeGroup MYINSTANCE : endifMYINSTANCE = NewInstance C_MYCLASS AllocGroup nullLocal Integer MY_STATUS[L]MY_STATUS = fmet MYINSTANCE.AINIT()If ([L]MY_STATUS >= [V]CST_AERROR)GMESSAGE = <a function to retrieve the most severe message error in the error stack will be available soon>GOK = 0 : GERR = 1ReturnEndif

Then you have to reset some fields in the UI depending on business rules and refresh the UI.

Raz [M:SCREEN1][M:SCREEN2]FIELD = DEFAULT_VALUEAffzo [M:SCREEN1]1-99Affzo [M:SCREEN2]FIELD

Next actions called are the same as for the creation process.

Delete actions

The deletion is very simple. You just have set ANOWRITE and to invoke the ADELETE method of your class.

AV_ANNULE

Before the deletion you have to declare and set again ANOWRITE to 1, because this part is - from a Supervisor point of view - disconnected from the rest of the process.

Local Integer ANOWRITEANOWRITE = 1
ANNULE

The class is supposed to be online because it has been read during the read action ($LIENS). Let's call the ADELETE method.

Local Integer MY_STATUS[L]MY_STATUS = fmet MYINSTANCE.ADELETE()If ([L]MY_STATUS >= [V]CST_AERROR)# Error managementGMESSAGE = <a function to retrieve the most severe message error in the error stack will be available soon>GOK = 0 : GERR = 1ReturnEndif

Summary

In Classic actionYou have to
OUVREset `ANOWRITE` to `1`
declare your instance
LIENSinstantiate the class
invoke the AREAD method
RAZCREinstantiate the class
initialize the class
update screen fields
CREATIONset instance properties
invoke the AINSERT method
APRES_CREupdate screen fields
MODIFupdate instance properties
invoke the AUPDATE method
APRES_MODupdate screen fields
RAZDUPinstantiate the class
initialize the class
reset some screen fields
AV_ANNULEdeclare and set `ANOWRITE` to `1`
ANNULEinvoke the ADELETE method
FERMEfree you instance

Header / lines management

In Classic you have two options to manage a header / lines object:

Because there is no line management in the Classic Framework, managing lines by hand means that you will have to code in $LIENS, $CREATION and $UPDATE both Classic array block management and the collection creation/udpate/delete line management.

Using TABLEAUX provides Supervisor tools to help with line management, especially with what happens on a line (added, updated or deleted) when an object is modified. TABLEAUX is not recommended if you have more than one collection to manage.

You will find bellow what you need to add to your code in addition to the code seen before to manage lines depending on the option you choose.

Managing lines by hand

It is strongly recommended to manage in your screens an invisible field named AUUID typed "A" length 36 for each array block corresponding to a collection. This field will help you to match array lines and collection elements.

LIENS

You have seen that in this action the Supervisor automatically loads the screens from the main table and you invoke the AREAD method of the class to load it. The class is loaded including its collections. As for a Classic development you have to set the array blocks from the subfiles.

# Setting nolign for the transclass from [F] to [M]nolign = 1For [F:FILE]INDEX Where [F:FILE]KEY = [F:MAINFILE]KEY[M:SCREEN] = [F:FILE]nolign += 1Next[M:SCREEN]NBLINE = nolign - 1

Note that the tranclass automatically set screen AUUIDs from file AUUIDs.

CREATION

First you have to clean your collection. This handles the case were you got an error during a first attempt, and you fixed it before trying again. If you don't clean the collection all of lines will be duplicated in the collection.

Local Integer IFor I=1 To maxtab(MYINSTANCE.MYCOLLECTION)If (fmet MYINSTANCE.ADELLINE("MYCOLLECTION", MYINSTANCE.MYCOLLECTION(I).AORDER) <> [V]CST_AOK)GMESSAGE = <a function to retrieve the most severe message error in the error stack will be available soon>GOK = 0 : GERR = 1ReturnEndifNext

Then before invoking the AINSERT method you have to create lines in the collection from lines in the screen

Local Integer I, INDEXFor I = 0 to [M:SCREEN]NBLINE - 1INDEX = fmet MYINSTANCE.ADDLINE("MYCOLLECTION", [V]CST_ALASTPOS)If (INDEX = [V]CST_ANOTDEFINED)# Error managementGMESSAGE = <a function to retrieve the most severe message error in the error stack will be available soon>GOK = 0 : GERR = 1BreakEndifnolign = I + 1# Use the SetInstanceSetInstance MYINSTANCE.MYCOLLECTION(INDEX) With [M:SCREEN]# Complete for non matching fields and translatable fieldsMYINSTANCE.MYCOLLECTION(INDEX).PROPERTY1 = [F:SUBFILE]FIELD1 MYINSTANCE.MYCOLLECTION(INDEX).PROPERTY2 = [M:SCREEN]ARRAY_FIELD1(I)...Nextif (GERR = 1) : Return : endif
MODIF

This may be the hardest part, because we have to detect if a line in the collection has been updated or deleted depending on the array block in the screen. As said, it is recommended to manage an AUUID as an invisible string field ("A" type, length 36) in the array block to help matching lines and collection elements (see code below). Remember you have to use the toUuid() function to convert a canonical uuid string to the uuident internal type.

Note that at the beginning of the following code, the ASTALIN for each element of the collection should always be "No changes" because nothing has been done on the class since it has been read in the $LIEN action.

Local Integer I, INDEXFor I = 0 to [M:SCREEN]NBLINE - 1# Retrieve the element using the screen's AUUIDINDEX = find(toUuid([M:SCREEN]AUUID(I)), MYINSTANCE.MYCOLLECTION(1..maxtab(MYINSTANCE.MYCOLLECTION)).AUUID)If (INDEX = 0)# We did not find the element matching with the array line.# We add a line.INDEX = fmet MYINSTANCE.ADDLINE("MYCOLLECTION", [V]CST_ALASTPOS)If (INDEX = [V]CST_ANOTDEFINED)# Error managementGMESSAGE = <a function to retrieve the most severe message error in the error stack will be available soon>GOK = 0 : GERR = 1breakEndifEndifnolign = I + 1# Use the SetInstanceSetInstance MYINSTANCE.MYCOLLECTION(INDEX) With [M:SCREEN]# Complete for non matching fields and translatable fieldsMYINSTANCE.MYCOLLECTION(INDEX).PROPERTY1 = [F:SUBFILE]FIELD1 MYINSTANCE.MYCOLLECTION(INDEX).PROPERTY2 = [M:SCREEN]ARRAY_FIELD1(I) ... Nextif (GERR = 1) : Return : endif# Now, all elements that don't have the status Updated or Created must be deleted, because it means they don't match with lines in the arrayFor I = 1 to maxtab(MYINSTANCE.MYCOLLECTION)If !(find(MYINSTANCE.MYCOLLECTION(I).ASTALIN, [V]CST_ANEW, [V]CST_AUPD)If (fmet MYINSTANCE.ADELLINE("MYCOLLECTION", MYINSTANCE.MYCOLLECTION(I).AORDER) <> [V]CST_AOK)# Error managementGMESSAGE = <a function to retrieve the most severe message error in the error stack will be available soon>GOK = 0 : GERR = 1BreakEndifEndifNextif (GERR = 1) : Return : endif
ANNULE

Invoking the ADELETE method as seen before is enough, it will manage the deletion of lines in the collection.

Using the TABLEAUX script

TABLEAUX is not recommended if you have more than one collection to manage.

You have to add some code in your actions, based on the TABLEAUX script which manages the line level in a Classic header / lines object.
When a gosub is done to TABLEAUX, you will get a callback for each line processed. It is very useful to manage in the callback your collection lines during CRUD operations.

OUVRE
Gosub DECLARE From TABLEAUX

A callback named "DEFLIG" is done on your script. It is used to declare the name of the line table and some criteria. Nothing to do for your hybrid management.

LIENS
Gosub LIENS From TABLEAUX

A callback named "LIENS_LIG" is done on your script. For example, you may want to add code to read extra data and feed some properties in each line of your collection.

CREATION

Before calling CREATION from the TABLEAUX script you have to clean your collection. It's in case you've tried to save, you've got an error, you've fixed the error and you save again. If you don't clean the collection all of lines will be duplicated in the collection.

Local Integer IFor I=1 To maxtab(MYINSTANCE.MYCOLLECTION)If (fmet MYINSTANCE.ADELLINE("MYCOLLECTION", MYINSTANCE.MYCOLLECTION(I).AORDER) <> [V]CST_AOK)GMESSAGE = <a function to retrieve the most severe message error in the error stack will be available soon>GOK = 0 : GERR = 1ReturnEndifNextGosub CREATION From TABLEAUX

A first callback named INICRE_LIG is done on your script. Nothing to do for your hybrid management. Then a callback named VALLIG with TRTLIG="C" is done, the mechanism is explained just below in the MODIF chapter.

MODIF

You have to call MODIF from TABLEAUX.

Gosub MODIF From TABLEAUX

A callback named VALLIG is done on your script. It is a very important callback because you will manage addition, deletion and update in your collection in this action.

$VALLIGCase TRTLIGWhen "C" : Gosub VALLIG_CREWhen "M" : Gosub VALLIG_UPD When "A" : Gosub VALLIG_DELEndcaseReturn

The "C" code means that the line has been added in the block. You have to create a element in the collection.

$VALLIG_CRE…Gosub INS_LINE_COLLECTION…Return$INS_LINE_COLLECTIONLocal Integer INDEXINDEX = fmet MYINSTANCE.ADDLINE("MYCOLLECTION", [V]CST_ALASTPOS)If (INDEX = [V]CST_ANOTDEFINED)# Error managementGMESSAGE = <a function to retrieve the most severe message error in the error stack will be available soon>GOK = 0 : GERR = 1ReturnEndif# nolign has already been set to the array line number# Use the SetInstanceSetInstance MYINSTANCE.MYCOLLECTION(INDEX) With [M:SCREEN]# Complete for non matching fields and translatable fieldsMYINSTANCE.MYCOLLECTION(INDEX).PROPERTY1 = [F:SUBFILE]FIELD1 MYINSTANCE.MYCOLLECTION(INDEX).PROPERTY2 = [M:SCREEN]ARRAY_FIELD1(nolign-1) ...Return

The "M" code means that the line has been modified. There is a "two passes" mechanism but we only need one of them. That's why we process the update only when SIGN = -1. We have to retrieve the matching element in the collection to update it. Note that the [F] line file has been automatically loaded by TABLEAUX script.

$VALLIG_UPDIf SIGN = -1…Gosub UPD_LINE_COLLECTION…EndifIf SIGN = +1…EndifReturn$UPD_LINE_COLLECTIONLocal Integer INDEX# The [F] file is loaded, the AUUID contains the unique reference to this recordINDEX = find([F:COL_FILE]AUUID, MYINSTANCE.MYCOLLECTION(1..maxtab(MYINSTANCE.MYCOLLECTION)).AUUID)If (INDEX > 0)# nolign has already been set to the array line number# Use the SetInstanceSetInstance MYINSTANCE.MYCOLLECTION(INDEX) With [M:SCREEN]# Complete for non matching fields and translatable fieldsMYINSTANCE.MYCOLLECTION(INDEX).PROPERTY1 = [F:SUBFILE]FIELD1 MYINSTANCE.MYCOLLECTION(INDEX).PROPERTY2 = [M:SCREEN]ARRAY_FIELD1(nolign-1) ...EndifReturn

The "A" code means that the line has been deleted. We have to delete the matching element. Because this callback is also done when the whole object is deleted, we have to test that GREP <> "A" before processing. It is useless to manage a line by line deletion when we will delete the whole object (see $ANNULE below).

$VALLIG_DEL…if (GREP <> "A")# We are not in the complete object deletionGosub DEL_LINE_COLLECTIONEndif…Return$DEL_LINE_COLLECTIONLocal Integer INDEXINDEX = find([F:COL_FILE]AUUID, MYINSTANCE.MYCOLLECTION(1..maxtab(MYINSTANCE.MYCOLLECTION)).AUUID)If (INDEX > 0)If (fmet MYINSTANCE.ADELLINE("MYCOLLECTION", MYINSTANCE.MYCOLLECTION(INDEX).AORDER) <> [V]CST_AOK)# Error managementGMESSAGE = <a function to retrieve the most severe message error in the error stack will be available soon>GOK = 0 : GERR = 1EndifEndifReturn
ANNULE
Gosub ANNULE From TABLEAUX

A callback named "VALLIG" is done on your script. But as explained above in $MODIF nothing will be done in the collection because of the case GREP = "A". The deletion of the header will delete automatically the collection. You don't have to delete each element of the collection.

Miscelleanous

Translatable texts

Translatable texts are not stored in the table linked to the class but in the ATEXTRA table. When you use a Setinstance this kind of field is not managed, you have to add code like:

SetInstance MYCLASS with [M:SCREEN]MYCLASS.TRAD_FIELD = [M:SCREEN]TRAD_FIELD

CLOB / BLOB

CLOB and BLOB may not be stored in the table linked to the class but in a dedicated table (which is recommended). In this case when you use a SetInstance this kinds of field are not managed, you have to add code like:

SetInstance MYCLASS with [M:SCREEN]MYCLASS.MYCLOB = [M:SCREEN]MYCLOB

Real Life Example

Of course, all of this article is a high level description of hybrid development. Real life is much more complex. Luckily, since Update 9, you can have a look at a real working hybrid developments.

Freight container, since Update 9

It is an interesting example because the container object has a header/lines datamodel and is managed by a class with a collection using the Supervisor TABLEAUX script.

Project (CRM), since Version 11

This example shows an header/lines object management using the "line management by hand" method.