2019 February Release

Implementing your Cloud AppPermanent link for this heading

Finally you’ve arrived at the chapter where we’re about to plunge knee deep into coding. So put your seat belt on, pull out the “An Introduction to Fabasoft app.ducx” white paper [Faba19a] and make sure you’re in the “Fabasoft app.ducx” perspective in Eclipse (if not, select “Window” > “Open Perspective” > “Other” > “Fabasoft app.ducx”).

If you carefully followed all the steps we described in the previous chapters, you should now have a skeleton project for your Cloud App in your Eclipse workspace and your screen should somehow look similar to what can be seen in the following figure.

Now, let the coding begin!

But wait a second… which programming language are we going to use to build our Cloud App?

Figure 22: Your Cloud App project in Eclipse

Introducing the domain-specific languages of Fabasoft app.ducxPermanent link for this heading

Fabasoft app.ducx is based on a set of different modeling languages referred to as domain-specific languages (DSLs), where each DSL was designed for addressing a certain aspect of Cloud App development:

  • The purpose of the app.ducx object model language is to define the persistent object model for your Cloud App, such as object classes and properties.
  • The app.ducx resource language allows you to define resources such as string objects, error messages and symbols. Using the app.ducx resource language, you can create culture- and language-independent solutions as it allows you to avoid hard-coded, culture- and language-specific literals in your solution.
  • The app.ducx user interface language allows you to define forms, form pages, menu items and other user interface elements for your object classes.
  • The purpose of the app.ducx use case language is to define and implement use cases, and provide method implementations for these use cases. Use cases can be implemented in Fabasoft app.ducx expression language or as so-called virtual applications.
  • The app.ducx business process language allows you to define the process model for your Cloud App in order to describe and manage workflows.
  • The purpose of the app.ducx customization language is to customize and tailor Fabasoft Cloud features provided out-of-the-box to the specific requirements of your Cloud App.
  • The app.ducx unit test language allows you to define unit tests, unit test groups and test scenarios in a convenient and efficient way.
  • Fabasoft app.ducx expression language is a distinct domain-specific language of Fabasoft app.ducx. Fabasoft app.ducx expressions can be embedded inline in an expression block in other domain-specific languages. Fabasoft app.ducx expression language is processed by the Fabasoft app.ducx compiler and transformed into Fabasoft app.ducx expressions, which are evaluated at runtime by the Fabasoft Folio Kernel.

In contrast to all the other DSLs of Fabasoft app.ducx, keywords, predefined functions and predefined variables in Fabasoft app.ducx expression language are not case sensitive.

[Faba19a] provides a comprehensive discussion of the syntax and grammar of the Fabasoft app.ducx DSLs, including Fabasoft app.ducx expression language and the query language for search queries.

Defining the object modelPermanent link for this heading

The object model is the first thing you have to define when building a Cloud App.

Using the app.ducx object model language, you can easily define the basic elements that make up the object model:

  • object classes
  • properties and fields
  • enumeration types
  • structures

Every object model element in Fabasoft app.ducx, and also the other persistent model elements yet to be presented in the following chapters (e.g. forms and form pages), must be assigned a unique reference (i.e. a programming name for a particular object class or property). These references should follow the reference naming conventions laid out in [Faba19a].

Object model elements may only be defined within an object model block in object model files with a .ducx-om extension. The objmodel keyword denotes an object model block. It must be followed by the reference of your Cloud App and curly braces.

You can organize the object model elements making up your Cloud App in as many object model files as you wish. However, the skeleton project created for your Cloud App already contains a file named model.ducx-om, which is intended to be used for defining the basic object model of your Cloud App, e.g. the Logbook and TripLog object classes that we will define further down the road.

Example

model.ducx-om

objmodel FSCLOGBOOK@111.100
{
  import COOSYSTEM@1.1;

}

In addition to the model.ducx-om file, your Cloud App also contains a file named app.ducx-om, which contains the definition of the app object representing your Cloud App. In the chapter “The finishing touches”, we will discuss the purpose of the app object and what else should be defined in the app.ducx-om file. But for now, let’s focus on the model.ducx-om file.

Example

app.ducx-om

objmodel FSCLOGBOOK@111.100
{
  import COOSYSTEM@1.1;
  import COODESK@1.1;
  import COOATTREDIT@1.1;

  instance App AppFSCLOGBOOK {
    
symbol = SymbolAppFSCLOGBOOK;
    appdescriptionstr  = {}

  }

}

Note: To create a new object model file for organizing your object model elements, select “File” > “New” > “Fabasoft app.ducx Object Model File”.

Components of a Cloud App: app configuration, app room and app dashboardPermanent link for this heading

In addition to the classes depicted in chapter “Introducing your first Cloud App”, we need to define some app framework components that are more or less the same for every Cloud App and allow for a smooth integration of your Cloud App into the Fabasoft Cloud.

Each Cloud App must define the following four basic app framework components in its object model:

  • The app object is an object representing your Cloud App for configuration purposes. By default, the app object is defined in the app.ducx-om file. In chapter “The finishing touches”, we will discuss the purpose of the app object and what else should be defined in the app.ducx-om file.
  • The app configuration allows you to define app-wide settings in a central configuration object. Furthermore, the app configuration handles app licensing and allows you define which users or groups are permitted to use your Cloud App.
  • The app room is roughly similar to a teamroom. It governs access permissions for the business objects assigned to the app room.
  • The app dashboard is automatically added to the Home screen of each user allowed to use your Cloud App. It provides a single point of entry to your Cloud App and displays relevant information in role-based widgets.

In the following sections, we will encounter these app framework components again at some point. But first we’ll focus on the object model elements needed for logging our trips.

Adding the ‘Trip’ structurePermanent link for this heading

First, we’re going to define a data structure for recording all the required trip information according to chapter “Introducing your first Cloud App”, i.e. the place, date and time of departure and arrival and so on.

Example

model.ducx-om

objmodel FSCLOGBOOK@111.100
{
  import COOSYSTEM@1.1;
  import FSCTERM@1.1001;

  struct Trip {
    datetime trpdepartureat;
    string trpdepartureplace;
    unsigned float(6,1) trpstartmileage;
    datetime trparrivalat;
    string trpdestinationplace;
    unsigned float(6,1) trpendmileage;
    unsigned float(6,1) trpmileage readonly(ui);
    timespan trpduration readonly(ui);
    TermComponentObject trptype;
    string trppurpose;
    User trpdriver;
    string trpdrivername;
    string trpvehicleid;
    boolean trpcanceled;
  }
}

In Fabasoft app.ducx, the struct keyword is used to define a compound type composed of members that can have different types.

To represent a single trip from A to B along with all the other required metadata, we defined a data structure called Trip, which is composed of 14 properties of various data types.

The basic data types like string, float, boolean and datetime largely behave just like in every other programming language with the exception that a float can hold up to 16 digits and a boolean may also be null.

A timespan is an integer number storing the number of seconds between two dates. In the example, the readonly(ui) property modifier suffix is applied to the trpduration property to turn it into a read-only field in the user interface.

An object pointer property is a property pointing to an instance of the object class provided in place of the data type. No explicit keyword is required for defining an object pointer property. Instead, the object class of the objects that shall be referenced by the object pointer property is used as data type.

For instance, in the trpdriver property you can select an instance of object class User. When you do so, a reference pointing to the selected user object is stored in the trpdriver property. Keep in mind that the trpdriver property does not store the actual user object itself, but just a pointer pointing to it. If you delete the user object, the pointer becomes invalid and will return null when you access it.

You probably noticed that in the example, the Trip structure contains two properties for storing driver information, trpdriver and trpdrivername. The purpose of this is that later on, we will allow the user recording a new trip to pick a user object in the trpdriver property, but then we will store only the user’s name in the trpdrivername property. If the selected user’s name is changed later on, the value stored in the trpdrivername property will remain unaffected.

Refer to [Faba19a] for a complete listing of all the data types supported by Fabasoft app.ducx as well as for a comprehensive discussion of property modifier suffixes and their effects.

Finally, there’s one more very important thing not to forget: As a Cloud App developer you are required to carefully document your source code in English language. To do so, you have to use the Javadoc syntax for documenting all of your object classes, data types, properties, use cases and so on.

For the sake of brevity, we omit these comments in most of the examples in the book. But you should document every relevant element in your source code in order to reach the documentation ratio target of 100 %, so your Cloud App can successfully pass the release process.

The following example demonstrates how to document your source code using Javadoc style comments.

Example

model.ducx-om

objmodel FSCLOGBOOK@111.100
{
  import COOSYSTEM@1.1;
  import FSCTERM@1.1001;

  /**
   * Structure for recording trip information in a trip log

   */

  struct Trip {
    /**
     * Date and time of departure

     */

    datetime trpdepartureat;

    /**
     * Descriptive name of the place of departure, e.g. address or landmark

     */

    string trpdepartureplace;

    /**
     * Odometer reading of the vehicle at the beginning of the trip

     */

    unsigned float(6,1) trpstartmileage;

    …
  }
}

Note: [Orac18b] provides a good reference on how to write comments in Javadoc style.

Using terms for the trip typePermanent link for this heading

The trptype property of the Trip structure is defined as an object pointer property allowing the user to select instances of object class FSCTERM@1.1001:TermComponentObject, commonly referred to as “Terms”.

Terms can be used to implement category or type selections so a user can choose from one or multiple terms representing a category, type or state, and make a selection.

The benefit over enumerations is that with terms you can allow users to extend the available choices with custom terms (by allowing Cloud users to create custom terms), whereas enumerations cannot be extended by users.

In the chapter “Restricting the selectable trip types”, we will limit the selectable objects to one of three predefined terms shipped with your Cloud App so users can only choose between “Private”, “Business” and “Commute” to describe the type of a particular trip. However, for now, no filter restrictions apply and all the existing terms accessible by users may be selected in the trptype property.

Adding software component referencesPermanent link for this heading

Whenever you either explicitly or implicitly reuse parts of the functionality provided by another software component (e.g. using an object class provided by another software component in one of your object pointer properties), you have to add a reference to this software component.

Therefore, we have to add a reference to software component FSCTERM@1.1001, which provides the TermComponentObject object class that we used to define the selectable objects for the trptype property. To add a reference to software component FSCTERM@1.1001, select “Add Reference” from the context menu of the “Software Component References” tree node of your project in Project Explorer. In the “Select Components” dialog box, select the software component FSCTERM@1.1001 and click “OK”. You may also select more than one software component at a time.

In order to be able to use the short reference TermComponentObject when referring to the FSCTERM@1.1001:TermComponentObject object class in your code, you must add an import declaration for software component FSCTERM@1.1001 as shown in the example.

For further information on how to manage software component references and import declarations refer to [Faba19a].

Figure 23: Adding a software component reference

Defining the ‘TripLog’ object classPermanent link for this heading

After having defined the Trip structure, we can now move on and define our first object class, the TripLog object class.

First, we have to decide which base class to derive it from. There are three options to pick from:

  • BasicObject is the right pick for simple objects holding some metadata.
  • CompoundObject should be selected when your object class has one or many object lists assigned for storing children, similar to a folder. Instances of compound objects also automatically show up as nodes in the tree view.
  • ContentObject should be picked when your object class is primarily intended for storing a document of some sort.

In our case, we’ll pick CompoundObject as a base class for the TripLog object class as we may want to add an object list for storing related documents to the trip log later on.

The TripLog object class will be used for storing all the trips of a given month in a property of the Trip structure. This property is assigned the reference trltrips.

In Fabasoft app.ducx, any property can either be a scalar (meaning that only a single value of the data type assigned to the property can be stored) or a list. Since we want to store all the trips of a month in one trip log object, the trltrips property must be defined as a list of Trip.

In addition to the trltrips property, the trip log also needs a state so that we can distinguish between open trip logs, where the user can still record additional trips, and closed trip logs, where no more changes are permitted.

To model the state of a trip log, we use an enumeration. First, we define the enumeration type TripLogState with two enumeration items, TLS_OPEN for open and TLS_CLOSED for closed trip logs. Then we define a property named trlstate of enumeration type TripLogState in the TripLog object class.

New trip logs should automatically be initialized with the TLS_OPEN state. To accomplish that, add the init keyword to the definition of the trlstate property and set it to TLS_OPEN.

Furthermore, we add two date properties, trlfrom and trluntil, to the trip log for storing the departure date of the first non-canceled trip and the arrival date of the last non-canceled trip recorded in the trip log. These properties serve as informational properties only and will be populated automatically.

The properties trltrips, trlstate, trlfrom, and trluntil must not be directly changed by the user in the GUI but only through appropriate use cases which we will define later on. Therefore, the readonly(ui) property modifier suffix is attached to both properties to prevent users from changing them in the GUI.

We also include another property of the Trip data structure in the trip log. The sole purpose of the trlnewtrip property is to allow users to enter information for recording a new trip. In a further step, we will implement a mechanism so that the information entered by a user is not saved in the trlnewtrip property itself but instead recorded in the trltrips property.

As you want to prevent users without a valid license for your Cloud App from accessing objects belonging to your Cloud App, you have to assign the app object representing your Cloud App (AppFSCLOGBOOK) to the COOATTREDIT@1.1:compapps property of the object classes of your Cloud App (see example).

Note: You can also explicitly trigger a license check in your code by invoking the COOATTREDIT@1.1:CheckLicense action on your app object. For example, an explicit license check makes sense when protecting use cases exposed by a web service.

Finally, trip logs should not be displayed in the tree view when a logbook is expanded. To prevent the instances of an object class derived from CompoundObject from showing up in the tree view, set the compound keyword to false.

The following example illustrates the progress made so far. For the sake of brevity, we will omit parts of the source code already shown in the previous example. Omissions are indicated by a line of dots.

Example

model.ducx-om

objmodel FSCLOGBOOK@111.100
{
  import COOSYSTEM@1.1;
  import FSCTERM@1.1001;

  struct Trip {
    …
  }

  enum TripLogState {
    TLS_OPEN = 1,
    TLS_CLOSED = 2
  }

  class TripLog : CompoundObject {
    
compapps = { AppFSCLOGBOOK }
    compound = false;
    TripLogState trlstate readonly(ui) {
      init = TLS_OPEN;
    }
    date trlfrom readonly(ui);
    date trluntil readonly(ui);
    Trip[] trltrips readonly(ui);
    Trip trlnewtrip;
  }
}

Defining the ‘Logbook’ object classPermanent link for this heading

The Logbook object class is the container for all trip logs belonging to a logbook. It’s basically the central element holding all the trip logs for a vehicle. We also want to be able to manage permissions on the logbook-level, e.g. in case you have to hand over your logbook to the accounting folks for having them perform an audit on it.

So, in a way a logbook is like a teamroom for trip logs. Therefore, we derive the Logbook object class from the FSCTEAMROOM@1.1001:AppRoom object class as it serves as a folder for trip logs where you also want to be able to manage access permissions.

Note: As the logbook will serve as app room for our Cloud App, all subordinated objects (mostly trip logs, but also any attachments uploaded in the remarks) will be assigned to the logbook and reference it in the FSCTEAMROOM@1.1001:objteamroom property. Objects stored in an object pointer or object list that is marked as an app room child using the child keyword will automatically be assigned to the corresponding app room. You can call the FSCTEAMROOM@1.1001:GetObjectRoom action to determine the app room an object belongs to.

The default room roles available for assigning access rights to users, organizational units and teams are the same as for a regular teamroom, which means you end up with the following three room roles: “Full Control”, “Change Access”, and “Read Access”. We could customize the available room roles using the CPGetRoomRoles customization point, but for our Cloud App we will stick with the defaults.

By default, all objects assigned to an app room inherit the ACL of the app room. For example, if you grant someone “Change Access” in the logbook, she will be able to edit the logbook’s metadata and create and edit trip logs.

Now let’s define the properties we need for the Logbook object class:

The logvehicleid property is a simple string storing the unique ID of the vehicle that the logbook is associated with. Most likely, you want to enter the plate numbers of your vehicle for this purpose. We arbitrarily limit the maximum number of characters to 25 and also turn the property into a required field by attaching the not null property modifier suffix. For required fields, users must enter values in the GUI in order to be able to save their changes.

In the logdescription property, users can enter some descriptive text for their logbooks.

Note: While simple strings are limited to a maximum of 254 characters, string lists (defined using the string[] keyword) can store any number of characters – within reason, that is. Don’t try to store ten terabytes of text in a string list just for the heck of it.

Now we need to define the logtriplogs property, which is required for storing the trip logs belonging to a logbook. We use the unique property modifier prefix to indicate that the list of trip logs must not contain duplicate entries.

Lastly, the child keyword must be set to true for the logtriplogs property to indicate the trip logs stored in this property are subordinated to the logbook.

Note: When an object is assigned to an app room (in our example this is the logbook), the objects referenced in an object pointer or object list property are not automatically assigned to the app room unless the child keyword has been set to true for the corresponding property.

Example

model.ducx-om

objmodel FSCLOGBOOK@111.100
{
  import COOSYSTEM@1.1;
  import FSCTEAMROOM@1.1001;
  import FSCTERM@1.1001;

  struct Trip {
    …
  }

  enum TripLogState {
    …
  }

  class TripLog {
    …
  }

  class Logbook : AppRoom {
    compapps = { AppFSCLOGBOOK }
    string(25) logvehicleid not null;
    string[] logdescription;
    unique TripLog[] logtriplogs {
      child = true;
    }
  }
}

Linking logbook and trip logsPermanent link for this heading

Up next, we’re going to establish a bi-directional connection between a logbook and its associated trip logs.

Why do we need this?

The logtriplogs property of a logbook is a pointer to the trip logs belonging to that logbook, and therefore establishes a link from the logbook to its associated trip logs.

However, in order to find our way back from a trip log to the logbook it belongs to, we need some additional functionality to allow us to determine the vehicle ID from the logbook when recording a new trip in a trip log.

In our example, the Logbook object class is derived from the FSCTEAMROOM@1.1001:AppRoom object class and trip logs are automatically assigned to the Logbook app room as the logtriplogs property is a child property. Therefore, all trip logs will automatically store a reference to the app room they belong to in the FSCTEAMROOM@1.1001:objteamroom property.

But for the sake of transparency we can also establish an explicit bi-directional connection between a logbook and its trip logs by linking two properties.

First, we need to add an object pointer property to the trip log for pointing to the logbook. In the second step, we will automatically populate the new property trllogbook when a trip log is created. The user should not be able to change the property in the GUI, so we add the readonly(ui) property modifier suffix to the property definition.

Example

model.ducx-om

objmodel FSCLOGBOOK@111.100
{
  import COOSYSTEM@1.1;
  import FSCTEAMROOM@1.1001;
  import FSCTERM@1.1001;

  struct Trip {
    …
  }

  enum TripLogState {
    …
  }

  class TripLog : CompoundObject {
    compapps = { AppFSCLOGBOOK }
    compound = true;
    Logbook trllogbook readonly(ui);
    …
  }

  class Logbook : AppRoom {
    …

  }
}

Secondly, using the link keyword we link both properties, trllogbook of the trip log and logtriplogs of the logbook, with each other.

The links ensure that the integrity of the relationship between linked objects is maintained automatically. Whenever a new trip log is added to or removed from the logtriplogs property of the logbook, this change is then reflected in the trllogbook property of the concerned trip log. Put bluntly, when you add a trip log to the logbook, a pointer to the logbook is stored in the trip log.

Example

model.ducx-om

objmodel FSCLOGBOOK@111.100
{
  import COOSYSTEM@1.1;
  import FSCTEAMROOM@1.1001;
  import FSCTERM@1.1001;

  struct Trip {
    …
  }

  enum TripLogState {
    …
  }

  class TripLog : CompoundObject {
    compapps = { AppFSCLOGBOOK }
    compound = true;
    Logbook trllogbook readonly(ui) {
      link = logtriplogs;
    }
    …
  }

  class Logbook : AppRoom {
    compapps = { AppFSCLOGBOOK }
    unique TripLog[] logtriplogs {
      link = trllogbook;
      child = true;
    }
    …

  }
}

Defining the app configurationPermanent link for this heading

Each Cloud App also has to provide an object class derived from the FSCTEAMROOM@1.1001:AppConfigurationRoom object class.

The app configuration is required for assigning app licenses to users and groups (e.g. teams, organizational units etc.). Furthermore, you can use the app configuration for the definition of app-wide settings.

By default, an app configuration allows you to assign one of two roles to users and groups:

  • App Administrator: This is the administration role that grants permissions to edit the app configuration and define administrative settings to its members.
  • App User: This role simply assigns an app license to its members and grants read access to the app configuration.

Example

model.ducx-om

objmodel FSCLOGBOOK@111.100
{
  import COOSYSTEM@1.1;
  import FSCTEAMROOM@1.1001;
  import FSCTERM@1.1001;

  /**
   * Logbook Configuration
Room
   */

  class LogbookConfigurationRoom : AppConfigurationRoom {
    compapps = { AppFSCLOGBOOK }
  }

  …
}

Defining the app dashboardPermanent link for this heading

A Cloud App should also provide an app dashboard, which must be derived from the FSCTEAMROOM@1.1001:AppDashboard object class.

The purpose of the app dashboard is to provide a single point of entry for users to your Cloud App. The app dashboard is automatically created and placed on the Home screen of a user when she is assigned a license for your Cloud App via the app configuration.

Which information you want to display in the app dashboard depends on the nature of your Cloud App and can also be depending on the user’s role.

App administrators can easily switch from the app dashboard to the app configuration by clicking the “Switch to Configuration” menu on the task pane of the app dashboard. For ordinary app users this menu item is not displayed in the app dashboard.

Example

model.ducx-om

objmodel FSCLOGBOOK@111.100
{
  import COOSYSTEM@1.1;
  import FSCTEAMROOM@1.1001;
  import FSCTERM@1.1001;

  /**
   * Logbook
Dashboard
   */

  class LogbookDashboard : AppDashboard {
    compapps = { AppFSCLOGBOOK }
  }

  …
}

Defining the language strings of your object model elementsPermanent link for this heading

Now that we have defined the key object model elements making up your Cloud App, it’s time to assign some meaningful names to all the object classes and properties.

To define the strings displayed in the GUI, open the resources folder of your Cloud App project. The resources folder contains a subfolder for each language you decided to support. Remember when you had to select the check boxes for your Cloud App’s supported languages in your Cloud App project in Fabasoft Cloud?

The reference of the respective languages is used as name of the language folders. For instance, the reference of the language object representing the English language is COOSYSTEM@1.1:LANG_ENGLISH. That’s why the folder for English in your Cloud App project is named LANG_ENGLISH. Don’t try to rename it since the folder name maps to the reference of the language object.

Within the LANG_ENGLISH folder, you will find all the multilingual resources for the English language and the same applies for every other supported language.

Figure 24: Editing the multilingual names of your object model elements

All multilingual strings for your object model elements, forms and form pages etc. are stored in the mlnames.lang file located underneath the language folder. The name mlnames.lang is hardcoded, so don’t rename this file either.

To define all the English strings for your Cloud App, double-click the mlnames.lang file and enter the desired strings for all the entries in the table. Do the same for all other supported languages.

Afterwards, it’s always a good idea to clean and recompile your Cloud App project. To do so, select “Clean” from the “Project” menu of Eclipse, select your Cloud App project and click “OK”. This will recompile the project from scratch to make sure that all changes you’ve made to the multilingual strings are reflected in the compiler output.

Figure 25: Cleaning the Cloud App project to recompile it from scratch

Instead of defining the multilingual names of your object classes and properties in the mlnames.lang file in the respective language folder, you can also use the properties view of Eclipse to enter the language strings for the element selected in the source code.

Figure 26: Defining a language string in the properties view

Defining the symbolsPermanent link for this heading

An integral part of a visually compelling Cloud App are the symbols used for instances of your object classes, form pages, menu items and so on.

Using the app.ducx resource language, you can define symbols and resources such as strings and error messages to avoid hard-coded, culture- and language-specific values in your Cloud App.

A resource model block consists of import declarations and resource model elements. The resources keyword denotes a resource model block. It must be followed by the reference of your Cloud App and curly braces.

Resource model blocks can only be contained in files with a .ducx-rs extension.

Your Cloud App project already contains a symbols.ducx-rs file where you can define all the symbols for your Cloud App.

For each symbol, you need to provide images in the following formats: a GIF image in 16×16 pixels, a PNG image in 256×256 pixels as well as regular and inverted SVG vector graphics.

The suggested location for image files is the resources/symbols folder in your Cloud App project. However, you may freely organize your images in subfolders.

There is already a predefined symbol for your Cloud App in the symbols.ducx-rs file with the respective images for the symbol residing in the resources/symbols folder. You can use the definition of the SymbolAppFSCLOGBOOK symbol as a template for the symbols we need to define for object class Logbook and TripLog: SymbolLogbook and SymbolTripLog.

Example

symbols.ducx-rs

resources FSCLOGBOOK@111.100
{
  import COOSYSTEM@1.1;
  import COODESK@1.1;

  symbol SymbolAppFSCLOGBOOK {
    symbolimages<symbolimageformat, content> = {
      { SF_GIF16,   file("resources/symbols/AppFSCLOGBOOK-GIF16.gif")  },
      { SF_PNG256,  file("resources/symbols/AppFSCLOGBOOK-PNG256.png") },
      { SF_SVG,     file("resources/symbols/AppFSCLOGBOOK-SVG.svg") },
      { SF_SVG,     file("resources/symbols/AppFSCLOGBOOK-SVGINVERTED.svg") }
    }
  }

  symbol SymbolLogbook {
    symbolimages<symbolimageformat, content> = {
      { SF_GIF16,   file("resources/symbols/Logbook-GIF16.gif")  },
      { SF_PNG256,  file("resources/symbols/Logbook-PNG256.png") },
      { SF_SVG,     file("resources/symbols/Logbook-SVG.svg") },
      { SF_SVG,     file("resources/symbols/Logbook-SVGINVERTED.svg") }
    }
  }

  symbol SymbolTripLog {
    symbolimages<symbolimageformat, content> = {
      { SF_GIF16,   file("resources/symbols/TripLog-GIF16.gif")  },
      …
    }
  }
}

We suggest using an external image editing program for creating the image files for your symbols. Once you’ve created the images, import them into your Cloud App project by selecting the symbols folder underneath the resources folder. Then select “Import” from the context menu of the symbols folder, in the “Import” dialog box select the “File System” import source from the “General” branch, click “Next” and finalize the import.

Now that we have defined our custom symbols for logbooks and trip logs, we’ll have to assign them to the respective object classes in one of the next steps. This will be covered in the chapter “Assigning a symbol to the ‘Logbook”.

Note: After defining and uploading new symbols into your Cloud App VDE, you need to restart the web services of your Cloud Sandbox for the new symbols to become visible. The reason for this is that the new image files new to be deployed to the image cache of the web service, which is only refreshed after a restart.

For further information on how to restart the web services of your Cloud Sandbox refer to the chapter “Working with the Cloud App VDE.

Designing the formsPermanent link for this heading

With the basic object model along with the required symbols for your object classes in place, we can now tackle the next step: For each object class of your Cloud App, we have to design a set of forms and form pages for displaying the properties.

All the user interface elements for your Cloud App, such as forms, form pages and menu items, are defined using the app.ducx user interface model language.

A user interface model block consists of import declarations and user interface model elements. The userinterface keyword denotes a user interface model block. It must be followed by the reference of your Cloud App and curly braces.

User interface model blocks can only be contained in files with a .ducx-ui extension.

Just as is the case with all other types of model files, you can have as many .ducx-ui files in your Cloud App project as you wish. Usually, it’s a good approach to create one for each object class.

Defining a form set for the ‘Logbook’ object classPermanent link for this heading

It’s time to create our first app.ducx user interface file!

From the “File” menu, select “New” and then “Fabasoft app.ducx User Interface File”. In the dialog box, enter logbook.ducx-ui in the File name field and click “Finish”.

Figure 27: Creating a new app.ducx user interface file

Next, we’re going to define a few forms in the logbook.ducx-ui file.

Forms serve as “containers” for form pages, which in turn contain the properties displayed in the GUI. In order to be displayed, forms must be bound to specific use cases, which serve as a kind of trigger. When a use case involving a user interface is invoked on an instance of one of your object classes, Fabasoft Cloud tries to locate a matching form for this use case in the form bindings you provided for your object class and displays the matching form. If no match is found, the default form from the base class of your object class is displayed instead.

The most common use cases involving a user interface are listed in the following table. When creating a new object class, you should provide form bindings for at least the first four use cases listed in the table.

Use case

Description

COOSYSTEM@1.1:ObjectConstructor

This use case is invoked when a new instance of an object class is created.

COOATTREDIT@1.1:ReadObjectAttributes

This use case is invoked when the properties of an object are read.

COOATTREDIT@1.1:EditObjectAttributes

This use case is invoked when the properties of an object are edited.

COOSEARCH@1.1:SearchObjects

This use case is invoked when the search dialog box is opened.

COODESK@1.1:DisplayOptions

The form assigned to this use case is used for allowing users to select columns when changing the column settings of an object list.

COODESK@1.1:ExploreObject

This use case is invoked when an object is opened in the explore view by selecting the Explore menu or by selecting a compound object in the tree.

COODESK@1.1:ExploreTree

The form assigned to this use case defines the object lists shown in the tree view when a compound object is expanded.

COODESK@1.1:WidgetView

The form assigned to this use case defines the information shown in the widget view of an app dashboard.

Table 2: Use cases for form bindings

So basically, for our Logbook object class we have to provide form bindings for the triggers ObjectConstructor, ReadObjectAttributes, EditObjectAttributes and SearchObjects. However, as we can map the same form for multiple triggers, we will end up defining just two different forms: a constructor form and another form that is used for the remaining three bindings.

A form is defined using the form keyword. The form pages that make up the form can either be defined inside of the form block or outside, underneath the user interface model block. To define a form page, the formpage keyword is used. Within a form page, a dataset block expresses which properties will be displayed on the form page.

By convention, the references of forms should begin with the Form prefix. Constructor forms should be prefixed with ConstructorForm, and search forms with SearchForm. Likewise, the references of form pages should begin with the Page prefix.

Our first form, FormLogbook, is a simple form comprised of two form pages, PageLogbook and PageLogbookTripLogs. It should be used for reading and editing the properties of a logbook as well as for defining search restrictions when searching for logbooks.

In the dataset block of PageLogbook, we explicitly list the properties to be displayed on the form page. In addition to the logvehicleid and logdescription properties, we also include the objname property, which stores the name of the logbook.

The purpose of the second form page, PageLogbookTripLogs, is to display the trip logs associated with the logbook. We also use the symbol keyword to assign a symbol to the form page. By default, the symbol of the object class is used for the first form page of a form, but using the symbol keyword you can assign custom symbols to the remaining form pages.

Example

logbook.ducx-ui

userinterface FSCLOGBOOK@111.100
{
  import COOSYSTEM@1.1;
  import COOATTREDIT@1.1;
  import COODESK@1.1;
  import COOSEARCH@1.1;
  import FSCVAPP@1.1001;

  form FormLogbook {
    formpage PageLogbook {
      dataset {
        objname;
        logvehicleid;
        logdescription;
      }
    }
    formpage PageLogbookTripLogs {
      dataset {
        logtriplogs;
      }
    }
  }
}

The second form we need to define is the constructor form for logbooks.

Generally, you want the user to provide values for the most important properties of your object class right when they are creating a new instance of it. However, the constructor form should present only the properties that make sense when creating new objects, while leaving out the ones that don’t provide any benefit yet.

  • Since our requirements state that when creating a logbook, a trip log must automatically be created within the logbook, it doesn’t make sense to display the list of trip logs in the constructor form.

The ConstructorFormLogbook form is merely reusing the existing PageLogbook form page.

Example

logbook.ducx-ui

userinterface FSCLOGBOOK@111.100
{
  import COOSYSTEM@1.1;
  import COOATTREDIT@1.1;
  import COODESK@1.1;
  import COOSEARCH@1.1;
  import FSCVAPP@1.1001;

  form FormLogbook {
    …
  }

  form ConstructorFormLogbook {
    PageLogbook;
  }
}

Another form we need to provide is the desk form, which is displayed when a user opens a logbook by clicking on it or navigating to it using the tree. In this case we simply want to display the list of trip logs associated with the logbook.

As our Logbook object class is derived from FSCTEAMROOM@1.1001:AppRoom, it inherits a number of form pages we do not care about. Therefore, we disable the inheritance of form pages using the inherit keyword.

Example

logbook.ducx-ui

userinterface FSCLOGBOOK@111.100
{
  import COOSYSTEM@1.1;
  import COOATTREDIT@1.1;
  import COODESK@1.1;
  import COOSEARCH@1.1;
  import FSCVAPP@1.1001;

  form FormLogbook {
    …
  }

  form ConstructorFormLogbook {
    …;
  }

  form DeskFormLogbook {
    inherit = false;
    formpage DeskPageLookbookTripLogs {
      dataset {
        logtriplogs;
      }
    }
  }
}

Next, we have to take care of the form binding to ensure that the forms we defined are actually invoked when a new logbook is created or opened for editing. To accomplish this, we need to extend object class Logbook with a form binding by adding a forms for block to our code.

Remember, we need to provide form bindings for the triggers ObjectConstructor, ReadObjectAttributes, EditObjectAttributes, SearchObjects, and ExploreObject (for our desk form). To simplify things a bit, Fabasoft app.ducx provides the default keyword, which automatically maps to ReadObjectAttributes, EditObjectAttributes and SearchObjects. Therefore, we end up making just three entries in the list of form bindings, one for ObjectConstructor, one for default, and another one for ExploreObject.

Note: In the chapter “Defining the columns for the ‘logtriplogs’ property”, we will demonstrate how to define a desk form with a form page containing custom column settings for the logtriplogs property.

Example

logbook.ducx-ui

userinterface FSCLOGBOOK@111.100
{
  import COOSYSTEM@1.1;
  import COOATTREDIT@1.1;
  import COODESK@1.1;
  import COOSEARCH@1.1;
  import FSCVAPP@1.1001;

  form FormLogbook {
    …
  }

  form ConstructorFormLogbook {
    …
  }

  forms for Logbook {
    default           { FormLogbook }
    ObjectConstructor { ConstructorFormLogbook }
    ExploreObject     { DeskFormLogbook }
  }
}

Assigning a symbol to the ‘Logbook’ object classPermanent link for this heading

In the chapter “Defining the symbols” we discussed how to define custom symbols for your object classes. However, these symbols won’t be displayed unless we assign them to your object classes.

Use the symbol keyword to assign SymbolLogbook to the object class directly within the class declaration in the model.ducx-om file.

Example

model.ducx-om

objmodel FSCLOGBOOK@111.100
{
  import COOSYSTEM@1.1;
  import FSCTEAMROOM@1.1001;
  import FSCTERM@1.1001;

  

  class Logbook : AppRoom {
    compapps = { AppFSCLOGBOOK }
    symbol = SymbolLogbook;
    …
  }

  …
}

Defining a form set and symbol for the ‘TripLog’ object classPermanent link for this heading

Basically, to define a form set for trip logs we just need to repeat the same steps discussed before.

Create a new Fabasoft app.ducx user interface file named triplog.ducx-ui and define a form with the reference FormTripLog consisting of just a single form page, PageTripLog.

In the dataset block of PageTripLog, specify the TripLog object class. This shortcut allows you to include all properties assigned to the TripLog object class instead of having to list them one by one. In the chapter “Layouting form pages using the form designer”, we will explain how to use the form designer of Fabasoft app.ducx to select the properties that are actually displayed when we define the layout for the form page.

Later on, we will define an automatic name build for trip logs, so there’s no need to include the objname property on the PageTripLog form page.

This time, a form binding for default is doing most of the job. We don’t have to provide a form binding for COOSYSTEM@1.1:ObjectConstructor, since users will not have to create trip logs manually but instead will use a wizard to do so – as we will see later on.

Also, we do provide a form binding for COODESK@1.1:ExploreObject, which will display FormTripLog on the right-hand pane when a trip log is selected in the tree view.

At last, we assign the SymbolTripLog symbol to the TripLog object class in the model.ducx-om file.

Example

model.ducx-om

objmodel FSCLOGBOOK@111.100
{
  import COOSYSTEM@1.1;
  import FSCTEAMROOM@1.1001;
  import FSCTERM@1.1001;

  

  class TripLog : CompoundObject {
    compapps = { AppFSCLOGBOOK }
    symbol = SymbolTripLog;
    …
  }
}

triplog.ducx-ui

userinterface FSCLOGBOOK@111.100
{
  import COOSYSTEM@1.1;
  import COOATTREDIT@1.1;
  import COODESK@1.1;
  import COOSEARCH@1.1;
  import FSCVAPP@1.1001;

  form FormTripLog {
    formpage PageTripLog {
      dataset {
        TripLog;
      }
    }

  }

  forms for TripLog {
    default       { FormTripLog }
    ExploreObject { FormTripLog }
  }
}

Layouting form pages using the form designerPermanent link for this heading

The form designer of Fabasoft app.ducx allows you to define a layout for your form pages using a GUI. To activate the form designer, switch from the Code pane to the Form Pages pane. The Palette contains all properties that are defined in the dataset block of the selected form page.

The form designer provides following features:

  • The properties can be adjusted within the form page by drag-and-drop.
  • Labels and fields can be spanned horizontally or vertically over multiple columns or lines.
  • Pressing and holding the Alt key allows you to select the label and to adjust it within the field. A label can be positioned left, right, at the top or at the bottom of a field.
  • A horizontal rule can be inserted by selecting it from the “Static Controls” block.
  • To filter the available properties use the Filter field. Clicking “x” deletes the filter.
  • Using the context menu, you can assign a control to a property (see [Faba19e]).

If you define a layout for a form page using the form designer, a layout block is automatically generated in the source code of the concerned Fabasoft app.ducx user interface file.

Note: Once defined, the layout block overrides the dataset block. If you don’t add a property listed in the dataset block to the graphical layout, it will not be displayed in the GUI. However, in order to become available in the palette, a property has to be included in the dataset block. Alternatively, if you list the reference of an object class in the dataset block, all the properties directly assigned to the object class become available in the palette. This does not include the properties of base classes though, which must be explicitly listed in the dataset block.

Defining the layout of the “PageTripLog” form pagePermanent link for this heading

To define a layout for the PageTripLog form page, activate the form designer by switching to the Form Pages pane and show the palette by clicking the arrow-shaped “Show Palette” button in the upper right corner.

Figure 28: Using the form designer of Fabasoft app.ducx

In the Active Page list on the left, select the PageTripLog form page. Then click on the FSCLOGBOOK@111.100:TripLog category in the palette to expand it.

In the list of available properties, click the trllogbook property to select it. Move the mouse pointer over the box on the white canvas and click on it to position the trllogbook property on the form page.

Repeat the previous step for all properties except trlnewtrip, which will not be included on the PageTripLog form page.

To position two properties side-by-side, select the first property, move the mouse pointer over the central anchor point of the left edge of the property and drag it to the right to resize it. Repeat this step for the second property, and then drag the second property next to the first one.

In order to remove a property from the canvas, select it and press the “Delete” key on your keyboard.

Modifying the columns of the “Trip” structurePermanent link for this heading

For object lists and structures, you can define the columns or properties displayed for the respective element by defining a so-called “detail layout”.

If you just place a structure (e.g. the trltrips property) on a form page without making any modifications, all of the properties that are part of the structure will be displayed on the form page.

However, if you want only a subset of the properties of a structure to be displayed on the form page then you can define a detail layout for the structure.

So let’s go ahead on define a detail layout for the trltrips property:

  1. If you haven’t done so already, place the trltrips property on the form designer canvas
  2. Select the trltrips property on the canvas
  3. In the palette, expand the FSCLOGBOOK@1.1001:Trip category
  4. Select the trpstartmileage property of the Trip structure in the palette, point your mouse over the trltrips property on the form designer canvas, and drop the trpstartmileage property
  5. Repeat the previous step for all properties of the Trip structure except trpdriver

Why did we skip the trpdriver property? Because this property will only be used for entering trip data. For displaying recorded trips, it’s of no use as it does not contain a value. When it comes to building the wizard for recording trips, we will explain that in greater detail.

After placing all the properties on the form designer canvas, switch back to the Code pane and have a look at the layout block that was generated for PageTripLog.

There’s one more thing we need to change in the code to make your form perfect: We want all the recorded trips to be sorted automatically to ensure that the most recently recorded trip is displayed on top of the list. To accomplish this, we use the sort and index keywords to define the default sort order for the trltrips property. In order to display the most recent trip on top, we simply sort the trpdepartureat property in descending order by setting the sort keyword to down. If you want to sort multiple properties of a list or structure, the index keyword can be used to define the order in which the sorting rules are applied.

When you’re done with the form page, the final version of the source code for FormTripLog should look like the following example.

Example

triplog.ducx-ui

userinterface FSCLOGBOOK@111.100
{
  import COOSYSTEM@1.1;
  import COOATTREDIT@1.1;
  import COODESK@1.1;
  import COOSEARCH@1.1;
  import FSCVAPP@1.1001;

  form FormTripLog {
    formpage PageTripLog {
      dataset {
        TripLog;
      }
      layout {
        // Auto-generated layout block
        row {
          FSCLOGBOOK@111.100:trllogbook;
        }
        row {
          FSCLOGBOOK@111.100:trlstate;
        }
        row {
          FSCLOGBOOK@111.100:trlfrom;
          FSCLOGBOOK@111.100:trluntil;
        }
        row {
          FSCLOGBOOK@111.100:trltrips {
            detail = layout {
              row {
                FSCLOGBOOK@111.100:trpstartmileage;
                FSCLOGBOOK@111.100:trpendmileage;
              }
              row {
                FSCLOGBOOK@111.100:trpdepartureat {
                  sort = down;
                  index = 1;
                }
                FSCLOGBOOK@111.100:trparrivalat;
              }
              row {
                FSCLOGBOOK@111.100:trpdepartureplace;
                FSCLOGBOOK@111.100:trpdestinationplace;
              }
              row {
                FSCLOGBOOK@111.100:trppurpose;
                FSCLOGBOOK@111.100:trptype;
              }
              row {
                FSCLOGBOOK@111.100:trpdrivername;
                FSCLOGBOOK@111.100:trpvehicleid;
              }
              row {
                FSCLOGBOOK@111.100:trpduration;
                FSCLOGBOOK@111.100:trpmileage;
              }
              row {
                FSCLOGBOOK@111.100:trpcanceled;
              }
            }
          }
        }
      }
    }
  }

  
}

Defining the columns for the ‘logtriplogs’ propertyPermanent link for this heading

We can also use the form designer to define the default column settings that are shown when an object list is displayed.

For example, when the list of trip logs belonging to a logbook is displayed, it would be nice to display some additional columns besides the name of the respective trip logs.

To accomplish that, we need to add a layout block to the PageLogbookTripLogs form page. Open the PageLogbookTripLogs form page in the form designer, select the logtriplogs property, press the “Delete” key to remove it from the form page and then add it to the form page again by selecting it in the palette and placing it on the canvas. This step is necessary to allow for the column settings to become editable.

Select the logtriplogs property and then select the objname property on the palette and place it inside of the logtriplogs property on the canvas. Repeat this step for the trlstate and the trltrips property.

After saving your changes, the form designer generates a layout block for the PageLogbookTripLogs form page that will show the objname property, the trlstate property and the trltrips property as separate columns when the logtriplogs list is displayed in the property editor.

Since we also want to use the same column settings for the logtriplogs list when a logbook is displayed on the desk, we need to define a desk form named DeskFormTripLog containing the PageLogbookTripLogs form page and provide a form binding for COODESK@1.1:ExploreObject, which will display DeskFormTripLog on the right-hand pane when a logbook is opened on the desk or selected in the tree view.

Example

logbook.ducx-ui

userinterface FSCLOGBOOK@111.100
{
  import COOSYSTEM@1.1;
  import COOATTREDIT@1.1;
  import COODESK@1.1;
  import COOSEARCH@1.1;
  import FSCVAPP@1.1001;

  …

  form FormLogbook {
    formpage PageLogbook {
      …
    }
    formpage PageLogbookTripLogs {
      symbol = SymbolTripLog;
      dataset {
        logtriplogs;
      }
      layout {
        // Auto-generated layout block
        row {
          FSCLOGBOOK@111.100:logtriplogs {
            detail = layout {
              row {
                COOSYSTEM@1.1:objname;
              }
              row {
                FSCLOGBOOK@111.100:trlstate;
              }
              row {
                FSCLOGBOOK@111.100:trltrips;
              }
            }
          }
        }
        row {
        }
      }
    }
  }

  …

  form DeskFormLogbook {
    // Reuse the existing "PageLogbookTripLogs" form page
    PageLogbookTripLogs;
  }

  forms for Logbook {
    default           { FormLogbook }
    ObjectConstructor { ConstructorFormLogbook }
    ExploreObject     { DeskFormLogbook }
  }
}

Beefing up a form pagePermanent link for this heading

layout blocks generated by the form designer are pretty powerful. And as usual, most of the power remains hidden under the hood.

In the layout block of a form page, you can override various settings for properties or define constraints specific to the particular form page.

For instance, you can add validation constraints and user interface change constraints, which we will cover later on in the chapter “Adding a wizard for recording a trip”.

But for now, we will keep it simple. All we want to accomplish is that the objname property becomes a required field where users must enter a value.

To turn a property into a required field on form page level, add a mustbedef expression to the property in the layout block. The expression must yield a Boolean return value.

The following example shows the revamped PageLogbook form page.

Example

logbook.ducx-ui

userinterface FSCLOGBOOK@111.100
{
  import COOSYSTEM@1.1;
  import COOATTREDIT@1.1;
  import COODESK@1.1;
  import COOSEARCH@1.1;
  import FSCVAPP@1.1001;

  form FormLogbook {
    formpage PageLogbook {
      dataset {
        objname;
        logvehicleid;
        logdescription;
      }
      layout {
        // Auto-generated layout block
        row {
          COOSYSTEM@1.1:objname {
            mustbedef = expression {
              true;
            }
          }
        }
        row {
          FSCLOGBOOK@111.100:logvehicleid;
        }
        row {
          FSCLOGBOOK@111.100:logdescription;
        }
      }
    }
    …
  }
  …
}

Committing your changes to the Subversion repositoryPermanent link for this heading

Safety first! The golden rule when it comes to committing your work to the Subversion repository of Fabasoft Cloud is simple: Do it regularly and often!

The checked in Fabasoft app.ducx project for your Cloud App is what counts at the end of the day. To be more precise, the Fabasoft app.ducx project in the Subversion repository is what will be submitted to Fabasoft once you send your Cloud App into the release process.

Don’t just commit all of your work once you’re completely done with your entire Cloud App. Commit your changes whenever you finish a block of work (e.g. an object class and its properties or a form set). Once committed, you can always roll back to the committed version in case of problems (e.g. data loss due to a crashed hard drive).

To commit your changes to the Subversion repository using TortoiseSVN, select your Cloud App folder in Microsoft Windows Explorer, open the context menu and select “SVN Commit”. In the dialog box, enter a comment describing the changes that you’ve made in the Message field and click “OK”.

Figure 29: Committing your changes to the Subversion repository

If you are prompted for your credentials, enter your Fabasoft Cloud credentials to proceed.

Uploading your Cloud App into the Cloud SandboxPermanent link for this heading

Hey, we’ve made pretty good progress!

Let’s have a look at your Cloud App in your Cloud Sandbox environment so we can actually see what we’ve accomplished so far!

But before we can see anything, we have to deploy your Cloud App into the Cloud Sandbox.

Whatever you do within Eclipse does not affect the Cloud Sandbox until you actually upload your changes.

Deploying your Cloud AppPermanent link for this heading

To upload your Cloud App, you have to create a launch configuration for your Fabasoft app.ducx project in Eclipse. Click “Run Configurations” on the “Run” menu to bring up the dialog box.

Figure 30: Creating a new launch configuration in Eclipse

In this dialog box, select the “Fabasoft app.ducx” section, click the “New launch configuration” symbol and enter a Name for the new launch configuration. In addition to this, select the Project by clicking “Choose”. Click “Apply” to save your settings, and “Run” to deploy and run your Cloud App project.

Once a Fabasoft app.ducx launch configuration has been created, you can select the existing launch configuration on the “Run as” menu to run your Cloud App project.

Note: You can also use Apache Ant to trigger Ant builds that automatically compile your Cloud App and deploy it into your Cloud Sandbox. For further information refer to chapter “Automating builds with Apache Ant.

Assigning your Cloud App to a Cloud OrganizationPermanent link for this heading

After successfully compiling your project and uploading it into your Cloud Sandbox, your browser is opened and you are taken to your Cloud Sandbox where you are requested to log in.

Log in using the “developer” account along with the password you assigned to the Cloud App VDE users (see chapter “Working with the Cloud App VDE”).

On your home screen, click on the “Domain Administration Tool” to open it. In the domain administration tool, click on “Organigram” and navigate into the “Organigram” list. Select “Refresh” from the background context menu to populate the list of available Cloud Organizations.

Your Fabasoft Cloud VDE comes preloaded with a number of test organizations you can use for testing your Cloud App. In this book, we’re going to use the “VDE Superior Apps 0001” Cloud Organization for testing our Cloud App.

Open the context menu of the “VDE Superior Apps 0001” Cloud Organization, select “Properties” and navigate to the “Apps” form page. Next, create a new entry in the “Licensed Apps” list and select your “Driver’s Logbook” Cloud App in the “App” property. Set the “Licensed From” property to the current date and enter a meaningful number in the “Quantity” property, e. g. “100” to assign 100 licenses for your Cloud App to your test organization. Then click “Next” to save your changes.

Figure 31: Assigning your Cloud App to a Cloud Organization

Now that we have assigned licenses for your Cloud App to a Cloud Organization, we can log in as the owner of the test organization and create an app configuration instance for your Cloud App.

Close your current browser session and reconnect to your Cloud Sandbox with the kimble0001 user account, who is the owner of the “VDE Superior Apps 0001” Cloud Organization.

When logging in as user kimble0001 the Welcome Screen dialog box shown that prompts you create a new app configuration for your Cloud App.

Figure 32: Welcome Screen

Click “Create Configuration” to create a new app configuration and click “Next”.

Figure 33: Creating an App Configuration

Your first glimpse of your Cloud AppPermanent link for this heading

After logging in to your Cloud Sandbox with the kimble0001 user account and creating an app configuration, you can go ahead and have a first look at your Cloud App!

A “Driver’s Logbook” app dashboard has been automatically added to your Home Screen.

The app dashboard is not of much use yet as we haven’t defined any object lists, forms or use cases for it yet, but as the app administrator you can click on “Switch to Configuration” to get to the app configuration and navigate to the “App Rooms” object list.

Open the background context menu in the “App Rooms” object list by clicking the right mouse button and select “New” to create a new logbook.

In the “Create Logbook” dialog box, enter a name for your new logbook and enter the plate numbers of your car in the Vehicle ID property. Then enter a brief description for the logbook in the Description property and click “Next”.

Figure 34: Creating a logbook

If you want, you can also go ahead and open the logbook with a click and click “New” to create a new trip log in the list of trip logs belonging to the logbook. Then open the properties of the trip log by selecting “Properties” from its context menu.

Well, that’s pretty much what we’ve achieved so far. It’s not the next “Cloud App of the Year” yet, but step by step we’ll get a little closer: In the next chapter we’ll add some cool functionality for recording and canceling trips!

Implementing the use casesPermanent link for this heading

How about some real coding after all of this forms stuff? Let’s talk about implementing methods! Or rather, what I should say: let’s talk about use cases!

A use case can be implemented on an object class either as a method (using Fabasoft app.ducx expression language) or as a virtual application.

If at any point your use case needs to present a user interface – such as a form or a dialog box – you are required to implement it as a virtual application. This is also the case when you invoke another use case requiring user interaction from your use case.

When no user interaction is required at all, you can (and generally should) implement your use case using Fabasoft app.ducx expression language.

Use cases and virtual applications are defined using the Fabasoft app.ducx use case language in files with a .ducx-uc extension.

A use case model block consists of import declarations, transaction variable declarations, and use case model elements. The usecases keyword denotes a use case model block. It must be followed by the reference of your Cloud App and curly braces.

Adding a wizard for recording a tripPermanent link for this heading

Now we will implement the centerpiece of the entire Cloud App: the wizard for recording new trips in a trip log.

Using the context menu or a tip, users should be able to invoke the wizard on a trip log, which will then allow them to enter all the required data for a trip. After some validation, this data should be recorded in the trltips property of the trip log.

To allow users to enter their trip data, we will display the trlnewtrip property in a dialog box of the wizard. When a user clicks “Record Trip”, the data entered in the trlnewtrip property will be retrieved to populate the trltips property, but the contents of the trlnewtrip property will not be stored. Instead, the trlnewtrip property will be reinitialized with default values and users can immediately record the next trip without having to reinvoke the wizard.

The following figure shows the wizard that we’re going to implement now.

Figure 35: Wizard for recording a new trip

Defining a menu use casePermanent link for this heading

To get started, we need to create a new Fabasoft app.ducx use case file named usecases.ducx-uc. In the use case file, we define a menu use case with the reference RecordTripWizard. In contrast to a simple use case, a menu use case is intended to be invoked using a menu item from the menu, context menu or task pane.

Note: For menu use cases, the Fabasoft app.ducx compiler implicitly generates a menu item with the same reference as the menu use case and the prefix Menu. In our example, a menu item with the reference MenuRecordTripWizard is generated automatically, so we don’t need to define it manually.

The symbol keyword allows you to assign a symbol to the use case, which will be displayed in the task pane. In our example, we’ll simply assign the ‘+’ symbol, which is part of the library of standard symbols provided by the COODESK@1.1 software component.

Using the accexec keyword, we specify an access type that is required for executing the use case. If the current user is not granted this access type by the object’s access control list (ACL), the menu item for this use case is automatically removed by the system and the user cannot execute the use case.

Note: The ACL of an object governs which users or teams get access to the object as well as the level of access they get (e.g. read access, change access or full control). In a Cloud App you can also define custom room roles and ACLs. For further information on room roles and ACLs refer to [Faba19a] and the Fabasoft Cloud online help.

With the variant keyword, we tell the Fabasoft app.ducx compiler that we want to implement the RecordTripWizard use case in object class TripLog.

The application statement instructs the Fabasoft app.ducx compiler to create a new virtual application, which will be called whenever the use case is invoked. The expression block within the virtual application is its “main function”, which is where the virtual application starts execution.

In the expression block of the virtual application, we have to check if the trip log is still in “open” state. If it’s not, we throw an error, which needs to be defined in a separate Fabasoft app.ducx resource file. In our example, this file is named errors.ducx-uc, but you can pick any name you like for your files.

The errormsg keyword (allowed within a resources block of a ducx-rs file) is used to define a custom error. Errors in Fabasoft Cloud are similar to exceptions in Java. The actual (language-specific) error message is specified in the mlnames.lang files for each language.

Use the COOSYSTEM@1.1:Print action to replace placeholders in an error message with the actual values.

Note: Optional method parameters can be skipped either by specifying the keyword null or by simply providing a comma (as seen in the following example).

Following the application block, we add a hints block with some method hints, which basically tell the system a few things about our use case. By specifying the MH_CHANGESOBJ method hint, we declare that the use case changes the object it is operating on (so the menu is removed for users who do not have change permissions) and by adding the MH_NEEDSCURRENTVERSION method hint we ensure that the use case cannot be executed while viewing an older version in the time travel feature.

The following example shows what we have accomplished so far. In the next section, we will learn how to define a dialog to interact with users.

For further information on how to define and implement menu use cases and how to raise and format errors, refer to [Faba19a].

Example

usecases.ducx-uc

usecases FSCLOGBOOK@111.100
{
  import COOSYSTEM@1.1;
  import COODESK@1.1;
  import FSCVAPP@1.1001;
  import FSCVENV@1.1001;
  import FSCVIEW@1.1001;

  /**
   *
Records a new trip
   */

  menu usecase RecordTripWizard on selected or container {
    // Assign a symbol to use case that is displayed in the context menu and task pane
    symbol = SymbolNew;
    // Require access type "Change Properties" to execute the use case
    accexec = AccTypeChange;
    // Provide an implementation in object class "TripLog"
    variant TripLog {
      // Implement use case "RecordTripWizard" as virtual application
      application {
        // The "main function" of the virtual application
        expression {
          // Check if the trip log is in "open" state
          if (cooobj.trlstate == TLS_OPEN) {
            // TODO: Initialize trip data and display dialog for recording trip
          }
          else {
            // Throw an error if the trip log is not in "open" state
            throw coort.SetError(
              #ErrTripLogClosed,
              #ErrTripLogClosed.Print(, cooobj.GetName()));
          }
        }
      }
      hints = {
        MH_CHANGESOBJ,
        MH_NEEDSCURRENTVERSION
      }
    }
  }
}

errors.ducx-rs

resources FSCLOGBOOK@111.100
{
  import COOSYSTEM@1.1;
  import COODESK@1.1;

  errormsg ErrTripLogClosed;
}

mlnames.lang

ErrTripLogClosed.errtext    Trip log "%1$s" is already closed and must not be changed.

Defining a dialogPermanent link for this heading

Alright, let’s add the GUI portion to our wizard. After all, every virtual application should have a GUI – otherwise there would be no point in implementing a use case as a virtual application.

Essentially, displaying a GUI in Fabasoft Cloud means showing a form or form page to the user along with some buttons, which are commonly referred to as “branches”. A so-called “dialog” serves as a container for the form or form page and the branches.

For our wizard, we add a dialog with the reference DialogRecordTrip. In the dialog, we display a form page showing the trlnewtrip property where users can enter their trip data. Then we create a new Fabasoft app.ducx user interface file named wizards.ducx-ui and define a form page named PageRecordTrip, which shows all the properties of the trlnewtrip structure except trpdrivername and trpcanceled.

As mentioned before, trpdrivername will be populated based on the user object selected in the trpdriver property, and there’s no point in displaying the trpcanceled property when recording a new trip since this property indicates if a trip was canceled.

Furthermore, notice that we turn all of the properties of the trlnewtrip structure into required fields (with the exception of the two read-only properties trpduration and trpmileage) by adding a mustbedef expression to the respective properties in the layout block. This way, a user has to provide values for all of the required properties when recording a new trip.

For information purposes, we also display the trltrips property on the PageRecordTrip form page, which is displaying all the trips recorded in the trip log in read-only mode.

Once the form page has been defined, we can go ahead and define the dialog by adding a dialog block to the virtual application.

Using the form keyword, you can assign a form or form page to a dialog. In our example, we assign the PageRecordTrip form page to DialogRecordTrip dialog.

Next, we need to declare the object on which the dialog is operating. The Fabasoft Folio vApp Engine provides a mechanism for automatically loading and storing property values from and to the target object. The target keyword is used to assign a target object to a dialog. Usually, the name of a variable holding the desired target object is specified as the target. Additionally the target can be defined as an app.ducx expression.

For our DialogRecordTrip dialog, we assign cooobj as the target object. cooobj is a special variable representing the “current object”, i.e. the object the RecordTripWizard use case is invoked on. More specifically, it’s the trip log object on which you invoke the context menu to record a new trip.

The description keyword allows you to define multilingual information text in the mlnames.lang files, which is then displayed in the dialog.

In the example, notice how we use the <~~> placeholders as part of the multilingual strings in the mlnames.lang files to incorporate expressions into the strings. By invoking the COOSYSTEM@1.1:Print action on StrRecordTrip, we can merge the multilingual string defined for StrRecordTrip into other strings. However, when embedding expression in the mlnames.lang files you always have to use the fully qualified reference to address component objects. Only COOSYSTEM@1.1 is optional.

Why do we include these <~~> placeholders in our strings? Well, if you decide to change a string later on then you only have to make the change in one spot.

But why don’t we do the same thing for FSCVENV@1.1001:StrCancel in the DialogRecordTrip. description string then? That’s because the string stored in FSCVENV@1.1001:StrCancel is prefixed with an ampersand to designate the “C” of “Cancel” as a hotkey, and we definitely don’t want to have “&Cancel” in our description string.

In the next step, we have to define the branches for our dialog.

The keyword cancelbranch allows you to define a “Cancel” branch for aborting the execution of a virtual application. A cancelbranch is set to ignore any user input and is implicitly assigned caption FSCVENV@1.1001:StrCancel. Moreover, the default branch expression coouser.Cancel() is implicitly assigned to a cancelbranch, which throws an exception to stop the execution of the virtual application.

The purpose of the second branch we add is to record the trip data entered by a user. Using the branch keyword, we define a new branch named BranchRecordTrip and assign a caption string to the branch.

For defining the StrRecordTrip caption string, we create a new Fabasoft app.ducx resource file named strings.ducx-rs, where we can define a new string object using the string keyword.

Remember that in order to pass the review, you must not use any hard-coded string literals in your code. All the multilingual strings used in your Cloud App must be contained in the mlnames.lang files. That’s why we define a string object for the caption string in the Fabasoft app.ducx resource file instead of simply assigning a string literal to the BranchRecordTrip branch.

Within the BranchRecordTrip branch, the expression keyword is used to define a branch handler, which is invoked when a user clicks on the branch. However, before we go ahead with defining the code for the branch handler we need to take care of invoking our dialog from the main function of the virtual application.

Expressions that are hosted within a virtual application or within a dialog can make use of the detachment operator -> to invoke another use case, a virtual application, or a dialog. For invoking a dialog, the detachment operator -> must be followed by the reference of the dialog to be displayed.

Using the detachment operator, you can invoke any of the dialogs defined in the application block of your virtual application. You can also invoke dialogs that have been defined in other applications for reasons of reusability. However, reusing dialogs is strongly discouraged since unlike use cases and virtual applications, they are not self-contained units with a defined interface.

In our wizard, we need to invoke the DialogRecordTrip dialog from the expression constituting the main function of our virtual application. However, before we actually invoke the dialog, we want to initialize the trlnewtrip structure with some default values by invoking the InitTrip action (which is yet to be defined) on the trip log.

Furthermore, in the branch handler expression of the BranchRecordTrip branch, we need to carry out the following steps:

  1. Store the trip data: For the recording of the trip data entered by the user in the trltrips property we will implement an action named RecordTrip in the TripLog object class. We will present the actual implementation of the RecordTrip action later on.
  2. Save the changes: In order to save all the changes made, we force a hard commit of the current transaction by invoking the CommitRoot action on the trip log.
  3. Reinitialize the trlnewtrip structure: Based on the data from the last recorded trip and the default values, we will reinitialize the trlnewtrip structure by invoking the InitTrip action on the trip log, which is also yet to be defined. This way, the user can instantly record another trip without having to the leave the wizard.

Alright, why do we need this CommitRoot and what is it all about?

The idea of our wizard is that a user can record multiple trips one after the other until they cancel out by clicking the “Cancel” branch, which triggers a coouser.Cancel(). This action rolls back all the changes in the current transaction and exits the virtual application. In order to make sure that a trip is saved when the user hits “Record Trip”, we force a commit of the current transaction.

The explicit CommitRoot is something you usually don’t need in your virtual applications, because when they are exited using a regular branch or a nextbranch, the current transaction is automatically committed.

Check out the chapter about virtual applications in [Faba19a] for more information on branches (including the nextbranch) and branch handlers.

Also, you may have noticed that we use the term “action” when referring to CommitRoot. Well, Fabasoft app.ducx supports different types of methods that can be invoked on an object:

  • An action is considered to be the declaration of a private method that is implemented on one or more object classes.
  • A use case is considered to be the declaration of a public method. It can provide different implementations on one or more object classes.

So which one should you pick for defining a method, use case or action? Generally, the answer is action, including so-called “get actions”, “set actions” and “display actions”.

Refer to [Faba19a] to get a better understanding of the difference between use cases and actions as well as for a discussion of get actions, set actions and display actions.

Example

usecases.ducx-uc

usecases FSCLOGBOOK@111.100
{
  import COOSYSTEM@1.1;
  import FSCVAPP@1.1001;
  import FSCVENV@1.1001;
  import FSCVIEW@1.1001;

  /**
   *
Records a new trip
   */

  menu usecase RecordTripWizard on selected or container {
    symbol = SymbolNew;
    accexec = AccTypeChange;
    variant TripLog {
      application {
        expression {
          if (cooobj.trlstate == TLS_OPEN) {
            // Initialize "trlnewtrip" structure
            cooobj.InitTrip();

            // Display dialog for recording trip
           ->DialogRecordTrip;
          }
          else {F
            throw coort.SetError(
              #ErrTripLogClosed,
              #ErrTripLogClosed.Print(, cooobj.GetName()));
          }
        }

        dialog DialogRecordTrip {
          form = PageRecordTrip;
          target = cooobj;
          description = {}

          cancelbranch;

          branch BranchRecordTrip default {
            caption = StrRecordTrip;
            expression {
              // Record the trip data entered by the user in the "trltrips" property
              cooobj.RecordTrip(cooobj.trlnewtrip);

              // Force a commit to save the changes
              coouser.CommitRoot();

              // Reinitialize "trlnewtrip" structure
              cooobj.InitTrip();
            }
          }
        }
      }
      hints = {
        MH_CHANGESOBJ,
        MH_NEEDSCURRENTVERSION
      }
    }
  }
}

strings.ducx-rs

resources FSCLOGBOOK@111.100
{
  import COOSYSTEM@1.1;
  import COODESK@1.1;

  string StrRecordTrip;
}

mlnames.lang

DialogRecordTrip.description Enter the required trip data and click
                             "<~#FSCLOGBOOK@111.100:StrRecordTrip.Print()~>" to
                             record the trip in the trip log or click "Cancel" to
                             abort the operation.
DialogRecordTrip.mlname      <~#FSCLOGBOOK@111.100:StrRecordTrip.Print()~>
StrRecordTrip.string         Record Trip

wizards.ducx-ui

userinterface FSCLOGBOOK@111.100
{
  import COOSYSTEM@1.1;
  import COOATTREDIT@1.1;
  import COODESK@1.1;
  import COOSEARCH@1.1;
  import FSCVAPP@1.1001;

  formpage PageRecordTrip {
    dataset {
      trlnewtrip;
      trltrips;
    }
    layout {
      // Auto-generated layout block
      row {
        FSCLOGBOOK@111.100:trlnewtrip {
          detail = layout {
            row {
              FSCLOGBOOK@111.100:trpstartmileage {
                mustbedef = expression {
                  true;
                }
              }
              FSCLOGBOOK@111.100:trpendmileage {
                mustbedef = expression {
                  true;
                }
              }
            }
            row {
              FSCLOGBOOK@111.100:trpdepartureat {
                mustbedef = expression {
                  true;
                }
              }
              FSCLOGBOOK@111.100:trparrivalat {
                mustbedef = expression {
                  true;
                }
              }
            }
            row {
              FSCLOGBOOK@111.100:trpdepartureplace {
                mustbedef = expression {
                  true;
                }
              }
              FSCLOGBOOK@111.100:trpdestinationplace {
                mustbedef = expression {
                  true;
                }
              }
            }
            row {
              FSCLOGBOOK@111.100:trppurpose {
                mustbedef = expression {
                  true;
                }
              }
              FSCLOGBOOK@111.100:trptype {
                mustbedef = expression {
                  true;
                }
              }
            }
            row {
              FSCLOGBOOK@111.100:trpdriver {
                mustbedef = expression {
                  true;
                }
              }
            }
            row {
              FSCLOGBOOK@111.100:trpduration;
              FSCLOGBOOK@111.100:trpmileage;
            }
          }
        }
      }
      row {
        FSCLOGBOOK@111.100:trltrips;
      }
    }
  }
}

Restricting the selectable trip typesPermanent link for this heading

In the trptype property, users should only be able to select one of three possible trip types: “Private”, “Business” or “Commute”.

To accomplish this task, we need to do three things:

  • Define three terms to represent the selectable trip types
  • Add a filter constraint (casually referred to as a “filter expression”) to the trptype property
  • Disable the ability for users to create new terms and to search for other terms in the trptype property

The three terms must be defined in a Fabasoft app.ducx object model file. Consequently, we create a new file named instances.ducx-om and add definitions for three instances of object class FSCTERM@1.1001:TermComponentObject. Also, in the mlnames.lang files we assign multilingual names to the three terms.

Next, we go back to the definition of the trptype property in the model.ducx-om file and using the filter keyword add a filter expression to the property returning the list of selectable values.

Using a controlstyle block, we can disable the ability for users to create new terms or search for existing terms in the trptype property.

To accomplish our requirements, we can add a controlstyle block directly to the definition of the property in question. In this case, the defined control styles will apply for every form page displaying the trptype property. If you wanted a particular set of control styles to apply only for a specific form page, you could also add a controlstyle block to the detail layout of the property on that form page.

For further information on filter constraints and controlstyle blocks refer to [Faba19a].

By default, the selected object’s symbol is always displayed in object pointer properties. In our example, we want to remove the symbol of the trptype property so it does not display the default symbol of the selected term object in front of the name of the trip type.

To accomplish this, we parameterize the COOATTREDIT@1.1:CTRLBase control with a control argument to disable the symbol. The COOATTREDIT@1.1:CTRLBase control is the default control for used rendering object pointer properties.

We can also customize the appearance of datetime properties by assigning the COOATTREDIT@1.1:CTRLDateTime control and providing control arguments. In our example, it doesn’t make much sense to include the seconds when recording the departure and arrival time. Therefore, we will parameterize the COOATTREDIT@1.1:CTRLDateTime control to omit the seconds for the trpdepartureat and trparrivalat properties.

Refer to [Faba19e] for further information on controls and their supported control arguments.

Example

instances.ducx-om

objmodel FSCLOGBOOK@111.100
{
  import COOSYSTEM@1.1;
  import FSCTERM@1.1001;

instance TermComponentObject TermPrivate {
    compapps = { AppFSCLOGBOOK }
  }

  instance TermComponentObject TermBusiness {
    compapps = { AppFSCLOGBOOK }
  }

  instance TermComponentObject TermCommute {
    compapps = { AppFSCLOGBOOK }
  }
}

mlnames.lang

TermBusiness.mlname          Business
TermCommute.mlname           Commute
TermPrivate.mlname           Private

model.ducx-om

objmodel FSCLOGBOOK@111.100
{
  import COOSYSTEM@1.1;
  import COOATTREDIT@1.1;
  import FSCTERM@1.1001;

  …

  struct Trip {
    …
    
datetime trpdepartureat {
      attrrepresentation<uiaction, controlargument> = {
        { CTRLDateTime, "omitseconds=true" }
      }
    }
    

    datetime trparrivalat {
      attrrepresentation<uiaction, controlargument> = {
        { CTRLDateTime, "omitseconds=true" }
      }
    }
    

    TermComponentObject trptype {
      filter = expression as attrfilterobjectexpr {
        // Filter the selectable terms to "Business", "Commute" and "Private"
        OBJECTLIST([#TermBusiness, #TermCommute, #TermPrivate]);
      }
      controlstyle = expression {
        [ControlStyle(CTRLSTYLE_DISABLECREATE), ControlStyle(CTRLSTYLE_DISABLESEARCH)];
      }
      attrrepresentation<uiaction, controlargument> = {

        { CTRLBase,
"ShowIcon=false" }
      }

    }

    …

  }

  …
}

Implementing the validation constraintsPermanent link for this heading

Our wizard still doesn’t conduct any validation of the data entered by a user. So let’s change that and add some validation constraints for checking the date and time of departure and arrival as well as the mileage information provided by the user!

Validation constraints can either be defined as part of a property definition or in the layout block of a form page. Since we only want our validation constraints to be triggered when a new trip is recorded, it’s sufficient to add them to the PageRecordTrip form page that is displayed by the RecordTripWizard virtual application.

Bear in mind that validation constraints – no matter if they are defined at property or form page level – are only executed in the GUI, i.e. when a user is interactively entering data in a dialog.

In the layout block of the PageRecordTrip form page, we define a total of four validation constraints to cover the following requirements:

  • The date and time of departure must be before the date and time of arrival.
  • The date and time of departure must be after the date and time of arrival of the last recorded, non-canceled trip, if trips have been recorded in the trip log already.
  • The starting mileage must be lower than the ending mileage.
  • The starting mileage must not be lower than the ending mileage of the last recorded, non-canceled trip, if trips have been recorded in the trip log already.

Note: In the implementation of the validation constraints the action GetLastTrip is called to retrieve the last recorded, non-canceled trip. We will implement this action later on in chapter “Implementing the ‘GetLastTrip’ action”.

The commented example illustrates how to implement these validation constraints. For further information on validation constraints refer to [Faba19a].

Example

wizards.ducx-ui

userinterface FSCLOGBOOK@111.100
{
  import COOSYSTEM@1.1;
  import COOATTREDIT@1.1;
  import COODESK@1.1;
  import COOSEARCH@1.1;
  import FSCVAPP@1.1001;

  formpage PageRecordTrip {
    …
    layout {
      // Auto-generated layout block
      row {
        FSCLOGBOOK@111.100:trlnewtrip {
          detail = layout {
            row {
              FSCLOGBOOK@111.100:trpstartmileage {
                mustbedef = expression {
                  true;
                }
                // Validation constraint for validating the starting mileage
                validate = expression {
                  // Get the starting and ending mileage entered by the user
                  float start = cooobj.trlnewtrip.trpstartmileage;
                  float end = cooobj.trlnewtrip.trpendmileage;

                  // Get the ending mileage of the last recorded, non-canceled trip
                  Trip lasttrip = cooobj.trllogbook.GetLastTrip();

                  float lastend = lasttrip.trpendmileage;

                  if (start != null && lastend != null && lastend > start) {
                    // Throw an error if the starting mileage entered by the user is
                    // lower than the ending mileage of the last recorded,
                    // non-canceled trip
                    throw #ErrStartMileageLastEndMileage;
                  }
                  else if (start != null && end != null && start >= end) {
                    // Throw an error if the ending mileage entered by the user is
                    // lower than the starting mileage
                    throw #ErrStartMileageEndMileage;
                  }
                  else {
                    // Return "true" to indicate that the validation was successful
                    true;
                  }
                }
              }
              FSCLOGBOOK@111.100:trpendmileage {
                mustbedef = expression {
                  true;
                }
                // Validation constraint for validating the ending mileage
                validate = expression {
                  // Get the starting and ending mileage entered by the user
                  float start = cooobj.trlnewtrip.trpstartmileage;
                  float end = cooobj.trlnewtrip.trpendmileage;

                  if (start != null && end != null && start >= end) {
                    // Throw an error if the ending mileage entered by the user is
                    // lower than the starting mileage
                    throw #ErrStartMileageEndMileage;
                  }
                  else {
                    // Return "true" to indicate that the validation was successful
                    true;
                  }
                }
              }
            }
            row {
              FSCLOGBOOK@111.100:trpdepartureat {
                mustbedef = expression {
                  true;
                }
                // Validation constraint for validating the date and time of departure
                validate = expression {
                  // Get the date and time of departure and arrival entered by the user
                  datetime depdate = cooobj.trlnewtrip.trpdepartureat;
                  datetime arrdate = cooobj.trlnewtrip.trparrivalat;

                  // Get the date and time of arrival of the last recorded,
                  // non-canceled trip
                  Trip lasttrip = cooobj.trllogbook.GetLastTrip();
                  datetime lastarrdate = lasttrip.trparrivalat;

                  if (depdate != null && lastarrdate != null &&

                    lastarrdate >= depdate) {
                    // Throw an error if the departure date entered by the user is
                    // before the arrival date of the last recorded, non-canceled trip
                    throw #ErrDepartureBeforeLastArrival;
                  }
                  else if (depdate != null && arrdate != null && arrdate <= depdate) {
                    // Throw an error if the arrival date entered by the user is before
                    // the departure date
                    throw #ErrArrivalBeforeDeparture;
                  }
                  else {
                    // Return "true" to indicate that the validation was successful
                    true;
                  }
                }
              }
              FSCLOGBOOK@111.100:trparrivalat {
                mustbedef = expression {
                  true;
                }
                // Validation constraint for validating the date and time of arrival
                validate = expression {
                  // Get the date and time of departure and arrival entered by the user
                  datetime depdate = cooobj.trlnewtrip.trpdepartureat;
                  datetime arrdate = cooobj.trlnewtrip.trparrivalat;

                  if (arrdate != null && depdate != null && arrdate <= depdate) {
                    // Throw an error if the arrival date entered by the user is before
                    // the departure date
                    throw #ErrArrivalBeforeDeparture;
                  }
                  else {
                    // Return "true" to indicate that the validation was successful
                    true;
                  }
                }
              }
            }
            
          }
          …
        }
      }
      …
    }
  }
}

errors.ducx-rs

resources FSCLOGBOOK@111.100
{
  import COOSYSTEM@1.1;
  import COODESK@1.1;

  …

  errormsg ErrDepartureBeforeLastArrival;
  errormsg ErrArrivalBeforeDeparture;
  errormsg ErrStartMileageLastEndMileage;
  errormsg ErrStartMileageEndMileage;
}

mlnames.lang

ErrDepartureBeforeLastArrival.errtext   Departure must be before arrival.
ErrArrivalBeforeDeparture.errtext       Departure must be after arrival of the last
                                        recorded, non-canceled trip.
ErrStartMileageEndMileage.errtext       Starting mileage must be lower than
                                        ending mileage.
ErrStartMileageLastEndMileage.errtext   Starting mileage must not be lower than ending
                                        mileage of the last recorded, non-canceled
                                        trip.

Implementing the user interface change constraintsPermanent link for this heading

Now we’ll focus on user interface change constraints, casually referred to as “UI change handlers”. These handy guys are invoked when a user changes a property on a form page by entering or selecting a value.

We’re going to use them to instantly calculate the duration of the trip and the trip mileage as users enter the required trip data.

Just like validation constraints, user interface change constraints can either be implemented at property or form page level. And once again, we decide to implement them at form page level, because we only want our calculation voodoo to happen in our RecordTripWizard virtual application.

The good thing is that the user interface change constraint we need to define for the trpendmileage property is exactly identical to the one for the trpstartmileage property, and the one for the trparrivalat property is identical to the expression for the trpdepartureat property. So we’ll omit the trpendmileage and trparrivalat properties in the following example. However, you still have to go the whole nine yards and define all four of the user interface change constraints.

Example

wizards.ducx-ui

userinterface FSCLOGBOOK@111.100
{
  import COOSYSTEM@1.1;
  import COOATTREDIT@1.1;
  import COODESK@1.1;
  import COOSEARCH@1.1;
  import FSCVAPP@1.1001;

  formpage PageRecordTrip {
    …
    layout {
      // Auto-generated layout block
      row {
        FSCLOGBOOK@111.100:trlnewtrip {
          detail = layout {
            row {
              FSCLOGBOOK@111.100:trpstartmileage {
                mustbedef = expression {
                  true;
                }
                validate = expression {
                  …
                }

                // User interface change constraint for updating the total mileage
                // when the starting mileage is changed
                change = expression {
                  // Get the starting and ending mileage entered by the user
                  float start = cooobj.trlnewtrip.trpstartmileage;
                  float end = cooobj.trlnewtrip.trpendmileage;

                  // Check if starting and ending mileage are valid, and if the
                  // ending mileage is higher than the starting mileage
                  if (start != null && end != null && end > start) {
                    // Calculate and store the total mileage of the trip
                    cooobj.trlnewtrip.trpmileage = end - start;
                  }
                  else {
                    // Empty the trip mileage property is case of incomplete or
                    // invalid data
                    cooobj.trlnewtrip.trpmileage = null;
                  }
                }
              }

              …
              FSCLOGBOOK@111.100:trpdepartureat {
                mustbedef = expression {
                  true;
                }
                validate = expression {
                  …
                }
                // User interface change constraint for updating the trip
                // duration when the date and time of departure is changed
                change = expression {
                  // Get the date and time of departure and arrival entered by the user
                  datetime depdate = cooobj.trlnewtrip.trpdepartureat;
                  datetime arrdate = cooobj.trlnewtrip.trparrivalat;

                  // Check if departure and arrival date are valid, and if the arrival
                  // date is after the departure date
                  if (arrdate != null && depdate != null && arrdate > depdate) {
                    // Calculate and store the duration of the trip
                    cooobj.trlnewtrip.trpduration = arrdate - depdate;
                  }
                  else {
                    // Empty duration property in case of incomplete or invalid data
                    cooobj.trlnewtrip.trpduration = null;
                  }
                }
              }
            }
            
          }
          …
        }
      }
      …
    }
  }
}

Implementing the ‘GetLastTrip’ actionPermanent link for this heading

In our validation constraints, we’ve been using the GetLastTrip action already to help us retrieve the last recorded, non-canceled trip. Now it’s time to actually implement it!

We’ll provide two implementations: one for the TripLog object class to get the last recorded, non-canceled trip recorded in the trip log (we’ll need that later on), and one for the Logbook object class to retrieve the last recorded, non-canceled trip from the entire logbook.

Example

usecases.ducx-uc

usecases FSCLOGBOOK@111.100
{
  import COOSYSTEM@1.1;
  import COOATTREDIT@1.1;
  import FSCVAPP@1.1001;
  import FSCVENV@1.1001;
  import FSCVIEW@1.1001;

  menu usecase RecordTripWizard on selected or container {
    …
  }

  GetLastTrip(retval Trip trip) {
    variant Logbook {
      expression {
        // Get all trip logs belonging to the logbook
        TripLog[] triplogs = cooobj.logtriplogs;
        // Get the last recorded, non-canceled trip from each trip log and sort
        // them by departure date in ascending order
        Trip[] trips = cooobj.Sort(triplogs.GetLastTrip(), true, [#trpdepartureat])[1];
        // Return the last trip in the list, which is the most recently recorded,
        // non-canceled trip in the logbook
        trip = trips[-1];
      }
    }
    variant TripLog {
      expression {
        // Get the recorded trips from the trip log and filter out the canceled ones,
        // then return the last list entry
        trip = cooobj.trltrips[!trpcanceled][-1];
      }
    }
  }
}

Implementing the ‘InitTrip’ actionPermanent link for this heading

Let’s add some more bells and whistles to our wizard and populate the trlnewtrip structure with initialization values to make it more convenient for users to fill out the required properties.

The following should be accomplished by the implementation of the InitTrip use case:

  • Initialize the trpstartmileage property with the value of the trpendmileage property of the most recent, non-canceled recorded trip.
  • Initialize the trpdriver property with the current user.
  • Initialize the trptype property with a default defined in the app configuration, otherwise use the “Business” term as a fallback.

The RecordTripWizard virtual application is already invoking the InitTrip action before showing the DialogRecordTrip dialog, so all that’s left to do is to provide the actual implementation of InitTrip as follows in the example.

Note: Whenever you plan on changing an object using Fabasoft app.ducx expression language, make sure you lock it first by invoking the COOSYSTEM@1.1:ObjectLock action on the object. There are a few exceptions where you don’t need to lock the object before you change it, because Fabasoft Cloud already locks it on your behalf (e.g. in the object constructor, a set action or a user interface change constraint). However, generally it’s better to play it safe and place a lock on an object before changing it.

Example

model.ducx-om

objmodel FSCLOGBOOK@111.100
{
  import COOSYSTEM@1.1;
  import FSCTEAMROOM@1.1001;
  import FSCTERM@1.1001;

  /**
   * Logbook Configuration
Room
   */

  class LogbookConfigurationRoom : AppConfigurationRoom {
    compapps = { AppFSCLOGBOOK }

    // Add a property for defining the default trip type to the app configuration
    TermComponentObject cfgdefaulttrptype {
      allow { TermComponentObject; }
      filter = expression as attrfilterobjectexpr {
        OBJECTLIST([#TermBusiness, #TermCommute, #TermPrivate]);
      }
      controlstyle = expression {
        [ControlStyle(CTRLSTYLE_DISABLECREATE), ControlStyle(CTRLSTYLE_DISABLESEARCH)];
      }
      attrrepresentation<uiaction, controlargument> = {

        { CTRLBase,
"ShowIcon=false" }
      }
    }
  }

  …
}

usecases.ducx-uc

usecases FSCLOGBOOK@111.100
{
  import COOSYSTEM@1.1;
  import COOATTREDIT@1.1;
  import FSCVAPP@1.1001;
  import FSCVENV@1.1001;
  import FSCVIEW@1.1001;

  menu usecase RecordTripWizard on selected or container {
    …
  }

  GetLastTrip(retval Trip trip) {
    …
  }

  InitTrip() {
    variant TripLog {
      expression {
        // Lock the trip log before making any changes
        cooobj.ObjectLock(true, true);
        // Make sure that the trip log is still in "open" state
        if (cooobj.trlstate == TLS_OPEN) {
          // Retrieve the last recorded, non-canceled trip from the logbook
          Trip lasttrip = cooobj.trllogbook.GetLastTrip();
          // Initialize the "trlnewtrip" structure
          cooobj.trlnewtrip = {
            // Initialize the "trpstartmileage" property with the value of the
            // "trpendmileage" property of the last recorded, non-canceled trip
            trpstartmileage : lasttrip.trpendmileage,
            // Initialize the "trpdepartureplace" property with the value of the
            // "trpdestinationplace" property of the last recorded, non-canceled trip
            trpdepartureplace : lasttrip.trpdestinationplace,
            // Initialize the "trpdriver" property of the "trlnewtrip" structure with
            // the current user, which is always available in the "coouser" variable
            trpdriver : coouser,
            // Initialize the "trptype" property of the "trlnewtrip" structure with
            // the default term defined in the app configuration or if nothing specified
            // use the term representing "Business" as a fallback to indicate that it is
            // a business trip
            trptype :
              LogbookConfiguration(cooobj.GetAppConfiguration()).cfgdefaulttrptype ?
                LogbookConfiguration(cooobj.GetAppConfiguration()).cfgdefaulttrptype :
                #TermBusiness
          };
        }
        else {
          throw coort.SetError(
            #ErrTripLogClosed,
            #ErrTripLogClosed.Print(, cooobj.GetName()));
        }
      }
    }
  }
}

configuration.ducx-ui

userinterface FSCLOGBOOK@111.100
{
  import COOSYSTEM@1.1;
  import COOATTREDIT@1.1;
  import COODESK@1.1;
  import COOSEARCH@1.1;
  import FSCTEAMROOM@1.1001;
  import FSCVAPP@1.1001;

  …

  // Add a form for the app configuration that displays the "cfgdefaulttrptype" property
  form FormLogbookConfigurationRoom {
    formpage PageLogbookConfigurationRoom {
      dataset {
        cfgdefaulttrptype;
      }
    }
  }

  forms for LogbookConfiguration {
    default { FormLogbookConfigurationRoom }
  }
}

Implementing the ‘RecordTrip’ actionPermanent link for this heading

The purpose of the RecordTrip action is to store the trip information passed to the trip parameter in the trltrips property of the trip log.

We add some level of protection against incomplete trip information by checking if all the required properties are populated with a value before performing the same checks also carried out by the validation constraints.

Then, for reasons of better usability, we add the most recent trip at the beginning of the list of recorded trips.

Instead of storing the object pointer referencing the selected user representing the driver, we store their name as a string in the trpdrivername property. This guarantees that the values stored in the trip log will not change once recorded, even if the driver is renamed afterwards.

The following example shows the full source code of the RecordTrip action.

Example

usecases.ducx-uc

usecases FSCLOGBOOK@111.100
{
  import COOSYSTEM@1.1;
  import COOATTREDIT@1.1;
  import FSCVAPP@1.1001;
  import FSCVENV@1.1001;
  import FSCVIEW@1.1001;

  menu usecase RecordTripWizard on selected or container {
    …
  }

  GetLastTrip(retval Trip trip) {
    …
  }

  InitTrip() {
    …
  }

  RecordTrip(Trip trip) {
    variant TripLog {
      expression {
        // Lock the trip log before making any changes
        cooobj.ObjectLock(true, true);

        // Check if all the required fields are populated
        if (trip.trparrivalat != null && trip.trpdepartureat != null &&
          trip.trppurpose != null && trip.trpstartmileage != null &&
          trip.trpendmileage != null && (trip.trpdriver != null ||
          trip.trpdrivername != null) && trip.trpdepartureplace != null &&
          trip.trpdestinationplace != null && trip.trptype != null) {
          // Retrieve the last recorded, non-canceled trip and get the date and time
          // of arrival as well as the ending mileage from it
          Trip lasttrip = cooobj.trllogbook.GetLastTrip();
          datetime lastarrdate = lasttrip.trparrivalat;
          float lastend = lasttrip.trpendmileage;

          if (trip.trpdepartureat != null && lastarrdate != null &&
            lastarrdate >= trip.trpdepartureat) {
            // Throw an error if the departure date entered by the user is before
            // the arrival date of the last recorded, non-canceled trip
            throw #ErrDepartureBeforeLastArrival;
          }
          else if (trip.trparrivalat <= trip.trpdepartureat) {
            // Throw an error if the arrival date entered by the user is before
            // the departure date
            throw #ErrArrivalBeforeDeparture;
          }
          else if (lastend > trip.trpstartmileage) {
            // Throw an error if the starting mileage entered by the user is lower
            // than the ending mileage of the last recorded, non-canceled trip
            throw #ErrStartMileageLastEndMileage;
          }
          else if (trip.trpstartmileage >= trip.trpendmileage) {
            // Throw an error if the ending mileage entered by the user is lower
            // than the starting mileage
            throw #ErrStartMileageEndMileage;
          }
          // Record the trip in the trip log if all the validations succeed
          else {
            if (trip.trpduration == null) {
              // Calculate trip duration if it is not set already
              trip.trpduration = trip.trparrivalat - trip.trpdepartureat;
            }

            if (trip.trpmileage == null) {
              // Calculate trip mileage if it is not set already
              trip.trpmileage = trip.trpendmileage - trip.trpstartmileage;
            }

            // Store the driver's name as string in the "trpdrivername" property and
            // empty the "trpdriver" property
            trip.trpdrivername = trip.trpdriver.GetName();
            trip.trpdriver = null;

            // Store the vehicle ID retrieved from the logbook
            trip.trpvehicleid = cooobj.trllogbook.logvehicleid;

            // Record the new trip at the beginning of the "trltrips" list
            cooobj.trltrips = trip + cooobj.trltrips;
          }
        }
        else {
          // Throw an error if incomplete trip information has been provided
          throw #ErrIncompleteTripInfo;
        }
      }
    }
  }
}

errors.ducx-rs

resources FSCLOGBOOK@111.100
{
  import COOSYSTEM@1.1;
  import COODESK@1.1;

  …

  errormsg ErrIncompleteTripInfo;
}

mlnames.lang

ErrIncompleteTripInfo.errtext           Incomplete trip information entered.

Discarding the values in the ‘trlnewtrip’ propertyPermanent link for this heading

What happens with the values in the trlnewtrip property when recording a new trip?

Well, since we don’t do anything special with them, the values entered by the user in the trlnewtrip structure are actually stored in the trip log object.

However, it doesn’t make sense to store data in the trlnewtrip structure as the only purpose of it is to allow users to enter values that should eventually be recorded in the trltrips property.

Therefore, we need to add a set action to empty the trlnewtrip structure before it’s persisted. Moreover, the trlnewtrip structure should also not be searchable, so we also have to add a so-called “search action”.

We can use the predefined search action COOSYSTEM@1.1:AttrSearchNotPossible to prevent searching in the trlnewtrip structure, but we’ll have to implement our own set action for emptying the trlnewtrip structure.

The following example demonstrates how to implement our requirements.

Example

model.ducx-om

objmodel FSCLOGBOOK@111.100
{
  import COOSYSTEM@1.1;
  import FSCTERM@1.1001;

  struct Trip {
    …
  }

  enum TripLogState {
    …
  }

  class TripLog : CompoundObject {
    …

    Trip trlnewtrip {
      // Add "AttrNewTripSet" as set action
      set = AttrNewTripSet;

      // Add "AttrSearchNotPossible" as search action
      search = AttrSearchNotPossible;
    }
  }

  class Logbook : AppRoom {
    …
  }
}

usecases.ducx-uc

usecases FSCLOGBOOK@111.100
{
  import COOSYSTEM@1.1;
  import COOATTREDIT@1.1;
  import FSCVAPP@1.1001;
  import FSCVENV@1.1001;
  import FSCVIEW@1.1001;

  menu usecase RecordTripWizard on selected or container {
    …
  }

  usecase InitTrip() {
    …
  }

  usecase RecordTrip(Trip trip) {
    …
  }

  // Define a set action for emptying a property so all the values entered by the user
  // in the property are discarded and nothing is stored
  AttrNewTripSet(parameters as AttrSetPrototype) {
    variant TripLog {
      expression {
        // Set the "value" parameter to "null" to discard all the entered values
        value = null;
      }
    }
  }
}

Adding the wizard to the context menu and task panePermanent link for this heading

So we’ve spent the last couple of pages describing how to implement this wizard for the recording of trips. But how are we going to invoke it? Well, either using the context menu or using the task pane of the trip log.

In the next step, we’ll add our menu use case to both the context menu and the task pane of the trip log. The easiest way to do this is to use predefined expansion points as illustrated by the example presented in this chapter.

Note: Using a condition block, you can define a precondition in Fabasoft app.ducx expression language that must be fulfilled for the menu items to be included in the context menu or task pane.

Example

triplog.ducx-ui

userinterface FSCLOGBOOK@111.100
{
  import COOSYSTEM@1.1;
  import COOATTREDIT@1.1;
  import COODESK@1.1;
  import COOSEARCH@1.1;
  import FSCVAPP@1.1001;

  …

  menus for TripLog {
    // - The "COODESK@1.1:MenuContextExpansion" expansion point allows you to add
    //   custom menu items to the context menu of the instances of an object class,

    // - The "COODESK@1.1:MenuTaskPaneExpansion" expansion point allows you

    //   to add custom menu items to the task pane of instances of an object class

    MenuContextExpansion, TaskPaneExpansion {
      RecordTripWizard {
        // Only display the "Record Trip" menu item if the trip log is in "open" state
        condition = expression {
          cooobj.trlstate == TLS_OPEN;
        }
      }
    }
  }
}

mlnames.lang


MenuRecordTripWizard.menustatetext      Records new trips in selected trip logs
MenuRecordTripWizard.mlname             Record Trip

Adding a wizard for canceling a tripPermanent link for this heading

Here’s the good news: The hardest part of our work has been done, and from hereon in on it’s only going to get easier! But there are still a few things that need to be taken care of. A wizard for canceling recorded trips, for instance.

Our requirements state that it must be possible for a user to cancel the last recorded, non-canceled trip of a trip log. This feature should resemble what you would do by crossing out the last line in a paper logbook, if it has been recorded in error.

The following figure shows the final product as we envisage it.

Figure 36: Wizard for canceling recorded trips

Here’s an outline of the tasks we need to complete for our trip cancelation wizard:

  • First, we have to define another menu use case named CancelTripWizard.
  • The menu use case needs to invoke a dialog (going by the name of DialogCancelTrip), which will display a form page with the reference PageCancelTrip.
  • We will encapsulate the code performing the actual cancelation of the last recorded trip in a separate action with the reference CancelLastTrip. This way, we can call the same action when canceling trips via a web service.
  • We must define another error message, ErrNoTripsToCancel, which will be thrown if the wizard is invoked but there are no recorded trips available to cancel.
  • Also, we need a new string for the caption of the “Cancel Trip” branch of the dialog, which will be assigned the reference StrCancelTrip.
  • We will use the form designer to rename the trlnewtrip property to “Last Recorded Trip” on the PageCancelTrip form page.
  • Finally, we will add our menu item for invoking the wizard to the context menu and task pane expansion points of the TripLog object class.

Since you are already a battle-hardened Fabasoft app.ducx veteran by now, we’ll do it all at once. Let the comments in the example guide you as they explain the rationale behind our code.

Example

usecases.ducx-uc

usecases FSCLOGBOOK@111.100
{
  import COOSYSTEM@1.1;
  import COOATTREDIT@1.1;
  import COODESK@1.1;
  import FSCVAPP@1.1001;
  import FSCVENV@1.1001;
  import FSCVIEW@1.1001;

  …

  menu usecase CancelTripWizard on selected or container {
    symbol = SymbolCancel;
    accexec = AccTypeChange;
    variant TripLog {
      application {
        // Define application global variables for the whole list of recorded trips and
        // the index of the entry we want to cancel. Application global variables are
        // kept in memory throughout the entire virtual application, whereas "regular"
        // variables are not transported from dialog to dialog. Refer to [Faba19a] for
        // further information on application global variables.
        Trip[] triplist;
        integer tripidx;

        expression {
          // Lock the trip log before making any changes
          cooobj.ObjectLock(true, true);

          // Check if the trip log is in "open" state
          if (cooobj.trlstate == TLS_OPEN) {
            // Get the last recorded, non-canceled trip in the trip log
            Trip lasttrip = cooobj.GetLastTrip();

            if (lasttrip) {
              // Populate the "trlnewtrip" property of the trip log with the last
              // recorded, non-canceled trip in order to display it to the user
              cooobj.trlnewtrip = lasttrip;

              // Invoke the confirmation dialog
              ->DialogCancelTrip;
            }
            else {
              // Throw an error if no non-canceled trip could be found
              throw coort.SetError(
                #ErrNoTripsToCancel,
                #ErrNoTripsToCancel.Print(, cooobj.GetName()));
            }
          }
          else {
            // Throw an error if the trip log is not in "open" state
            throw coort.SetError(
              #ErrTripLogClosed,
              #ErrTripLogClosed.Print(, cooobj.GetName()));
          }
        }

        // Display the dialog in read-only mode
        dialog DialogCancelTrip readonly {
          form = PageCancelTrip;
          target = cooobj;
          description = {}

          cancelbranch;

          nextbranch {
            caption = StrCancelTrip;
            symbol = SymbolWastebasket;

            expression {
              // Invoke the action performing the actual cancelation of the trip
              cooobj.CancelLastTrip();
            }
          }
        }
      }
      hints = {
        MH_CHANGESOBJ,
        MH_NEEDSCURRENTVERSION
      }
    }
  }

  …

  CancelLastTrip() {
    variant TripLog {
      expression {
        // Make sure that the trip log is locked before making any changes
        cooobj.ObjectLock(true, true);

        // Check if the trip log is in "open" state
        if (cooobj.trlstate == TLS_OPEN) {
          // Get the last recorded, non-canceled trip
          Trip[] trips = cooobj.trltrips;
          Trip lasttrip = trips[!trpcanceled][-1];

          if (lasttrip != null) {
            // Set the last recorded trip to "canceled"
            lasttrip.trpcanceled = true;

            // Save the updated list of trips in the trip log object
            cooobj.trltrips = trips;
          }
          else {
            // Throw an error if no non-canceled trip could be found
            throw coort.SetError(
              #ErrNoTripsToCancel,
              #ErrNoTripsToCancel.Print(, cooobj.GetName()));
          }
        }
        else {
          // Throw an error if the trip log is not in "open" state
          throw coort.SetError(
            #ErrTripLogClosed,
            #ErrTripLogClosed.Print(, cooobj.GetName()));
        }
      }
    }
  }
}

wizards.ducx-ui

userinterface FSCLOGBOOK@111.100
{
  import COOSYSTEM@1.1;
  import COOATTREDIT@1.1;
  import COODESK@1.1;
  import COOSEARCH@1.1;
  import FSCVAPP@1.1001;

  …

  formpage PageCancelTrip {
    dataset {
      trlnewtrip;
    }
    layout {
      // Auto-generated layout block
      row {
        FSCLOGBOOK@111.100:trlnewtrip {
          detail = layout {
            row {
              FSCLOGBOOK@111.100:trpstartmileage;
              FSCLOGBOOK@111.100:trpendmileage;
            }
            row {
              FSCLOGBOOK@111.100:trpdepartureat;
              FSCLOGBOOK@111.100:trparrivalat;
            }
            row {
              FSCLOGBOOK@111.100:trpdepartureplace;
              FSCLOGBOOK@111.100:trpdestinationplace;
            }
            row {
              FSCLOGBOOK@111.100:trppurpose;
              FSCLOGBOOK@111.100:trptype;
            }
            row {
              FSCLOGBOOK@111.100:trpdrivername;
            }
            row {
              FSCLOGBOOK@111.100:trpduration;
              FSCLOGBOOK@111.100:trpmileage;
            }
          }
        }
      }
    }
  }

  …

  menus for TripLog {
    …

    MenuContextExpansion, TaskPaneExpansion {
      RecordTripWizard {
        condition = expression {
          cooobj.trlstate == TLS_OPEN;
        }
      }
      CancelTripWizard {
        condition = expression {
          // Display the "Cancel Trip" menu item only if the trip log is in "open"
          // state and if it contains non-canceled trips
          cooobj.trlstate == TLS_OPEN && cooobj.GetLastTrip() != null;
        }
      }
    }
  }
}

strings.ducx-rs

resources FSCLOGBOOK@111.100
{
  import COOSYSTEM@1.1;
  import COODESK@1.1;

  …

  string StrCancelTrip;
}

errors.ducx-rs

resources FSCLOGBOOK@111.100
{
  import COOSYSTEM@1.1;
  import COODESK@1.1;

  …

  errormsg ErrNoTripsToCancel;
}

mlnames.lang

DialogCancelTrip.description            If you want to cancel the displayed trip click
                                        "<~#FSCLOGBOOK@111.100:StrCancelTrip.Print()~>"
                                        to continue or click "Cancel" to abort the
                                        operation.
DialogCancelTrip.mlname                 Cancel Trip
ErrNoTripsToCancel.errtext              There are no trips to cancel in trip
                                        log "%1$s".
MenuCancelTripWizard.menustatetext      Cancels the last recorded trip in selected trip
                                        logs
MenuCancelTripWizard.mlname             Cancel Trip

The following figure shows how to use the form designer to rename a property on a form page. For further details on how to use the form designer refer to [Faba19a].

Figure 37: Renaming a property in the form designer

All done! You now have a fully working wizard for canceling recorded trips. Piece of cake, wasn’t it?

Adding a wizard for closing a trip logPermanent link for this heading

The last virtual application we’re going to implement is the wizard for closing a trip log so that users can no longer record any trips in it.

So what’s the point of this feature? Well, we don’t want users to be able to make changes to a trip log after a certain event has occurred (e.g. sending a trip log to the department head for approval).

The wizard follows the same principle as the wizards for recording and canceling trips, so implementing it won’t be a challenge for you.

Figure 38: Wizard for closing a trip log

As this is mainly a repetition of the concepts already covered before, we’ve omitted most of the comments in the example code.

Example

usecases.ducx-uc

usecases FSCLOGBOOK@111.100
{
  import COOSYSTEM@1.1;
  import COOATTREDIT@1.1;
  import COODESK@1.1;
  import FSCVAPP@1.1001;
  import FSCVENV@1.1001;
  import FSCVIEW@1.1001;

  …

  menu usecase CloseTripLogWizard on selected or container {
    symbol = SymbolLock;
    accexec = AccTypeChange;
    variant TripLog {
      application {
        expression {
          cooobj.ObjectLock(true, true);

          if (cooobj.trlstate == TLS_OPEN) {
            ->DialogCloseTripLog;
          }
          else {
            throw coort.SetError(
              #ErrTripLogClosed,
              #ErrTripLogClosed.Print(, cooobj.GetName()));
          }
        }

        dialog DialogCloseTripLog readonly {
          // Display the same form page that is also used for reading/editing the
          // properties of a trip log
          form = PageTripLog;

          target = cooobj;
          description = {}

          cancelbranch {
            caption = StrNo;
            symbol = SymbolNo;
          }

          nextbranch {
            caption = StrYes;
            symbol = SymbolYes;
            expression {
              // Set the state of the trip log to "closed"
              cooobj.trlstate = TLS_CLOSED;
              // Finalize the trip log so it cannot be changed anymore
              cooobj.ObjectFinalFormSet();
            }
          }
        }
      }
      hints = {
        MH_CHANGESOBJ,
        MH_NEEDSCURRENTVERSION
      }
    }
  }
}

wizards.ducx-ui

userinterface FSCLOGBOOK@111.100
{
  import COOSYSTEM@1.1;
  import COOATTREDIT@1.1;
  import COODESK@1.1;
  import COOSEARCH@1.1;
  import FSCVAPP@1.1001;

  …

  menus for TripLog {
    MenuContextExpansion, TaskPaneExpansion {
      …

      CloseTripLogWizard {
        condition = expression {
          cooobj.trlstate == TLS_OPEN;
        }
      }
    }

  }
}

mlnames.lang

DialogCloseTripLog.description          Click "Yes" to close this trip log or "No" to
                                        abort the operation.
DialogCloseTripLog.mlname               Close Trip Log "<~cooobj.GetName()~>"
MenuCloseTripLogWizard.menustatetext    Closes the selected trip logs
MenuCloseTripLogWizard.mlname           Close Trip Log

Adding a display action to show the number of recorded tripsPermanent link for this heading

In this chapter, we will show you how to implement a display action for the trltrips property of a trip log. The purpose of the display action is to render a string for the trltrips property that is shown when the property is displayed as a column of a list.

By default, when the trltrips property is displayed as a column of a list, the name of the property (“Recorded Trips”) and the number of entries in the list of recorded trips (in square brackets) is shown (e.g. “Recorded Trips, ... [3]” if there are three recorded trips).

As this default display string is not all that appealing, we will improve the user experience by defining our own display action, which will display “1 trip” instead of “Recorded Trips, ... [1]” (in case of a single entry in the list of recorded trips) or “3 trips” instead of “Recorded Trips, ... [3]” (in case of three recorded trips).

Figure 39: Displaying a property as a list column

Note: Refer to chapter “Defining the columns for the ‘logtriplogs’ property” to learn how to define the default column settings for the lists of your Cloud App’s object classes.

The following example documents all the necessary steps for the implementation of the display action. In addition to the display action, we also need to define two string objects for the multilingual string literals. Furthermore, we use the display keyword to assign the display action to the trltrips property.

Example

model.ducx-om

objmodel FSCLOGBOOK@111.100
{
  import COOSYSTEM@1.1;
  import FSCTERM@1.1001;

  …

  class TripLog : CompoundObject {
    …

    Trip[] trltrips readonly(ui) {
      // Assign the "AttrTripsGetDisp" display action to the "trltrips" property
      display = AttrTripsGetDisp;
    }

    …
  }

  …
}

usecases.ducx-uc

usecases FSCLOGBOOK@111.100
{
  import COOSYSTEM@1.1;
  import COOATTREDIT@1.1;
  import COODESK@1.1;
  import FSCVAPP@1.1001;
  import FSCVENV@1.1001;
  import FSCVIEW@1.1001;

  …

  AttrTripsGetDisp(parameters as AttrGetDispPrototype) {
    variant TripLog {
      expression {
        // Retrieve the number of entries in the list of recorded trips and filter out
        // the canceled entries
        integer tripcount = count(cooobj.trltrips[!trpcanceled]);

        // Use the COOSYSTEM@1.1:Print action to format the number of trips into the
        // appropriate string object holding the correct multilingual string and
        // return the formatted string in the output parameter named "string"
        string = (tripcount == 1)?
          #StrDisplayFormatSingleTrip.Print(, tripcount) :
          #StrDisplayFormatMultipleTrips.Print(, tripcount);
      }
    }
  }
}

strings.ducx-rs

resources FSCLOGBOOK@111.100
{
  import COOSYSTEM@1.1;
  import COODESK@1.1;

  …

  string StrDisplayFormatMultipleTrips;
  string StrDisplayFormatSingleTrip;
}

mlnames.lang

StrDisplayFormatMultipleTrips.string    %1$d trips
StrDisplayFormatSingleTrip.string       %1$d trip

Calculating the date of the first and last entry in a trip logPermanent link for this heading

The next challenge we’re going to tackle is the updating of the trlfrom and trluntil properties when the list of recorded trips is changed.

The trlfrom property must be populated with the departure date of the oldest recorded, non-canceled trip in the trltrips property and the trluntil property must contain the arrival date of the most recently recorded, non-canceled trip.

To accomplish this task, we define and implement a set action named AttrTripsSet and attach it to the trltrips property, as illustrated by the following example.

Since we’re already modifying the behavior of the trltrips property, we can at the same time add a controlstyle block for cosmetic reasons in order to prevent the “Copy rows” toolbar button from being displayed in the property editor as it doesn’t make sense to allow users to copy recorded trip entries to the clipboard.

Example

model.ducx-om

objmodel FSCLOGBOOK@111.100
{
  import COOSYSTEM@1.1;
  import FSCTERM@1.1001;

  …

  class TripLog : CompoundObject {
    …

    Trip[] trltrips readonly(ui) {
      // Assign the "AttrTripsSet" set action to the "trltrips" property
      set = AttrTripsSet;
      display = AttrTripsGetDisp;
      // Do not display the "Copy rows" toolbar button
      controlstyle = expression {
        ControlStyle(CTRLSTYLE_DISABLECREATE);
      }
    }

    …
  }

  …
}

usecases.ducx-uc

usecases FSCLOGBOOK@111.100
{
  import COOSYSTEM@1.1;
  import COOATTREDIT@1.1;
  import COODESK@1.1;
  import FSCVAPP@1.1001;
  import FSCVENV@1.1001;
  import FSCVIEW@1.1001;

  …

  AttrTripsSet(parameters as AttrSetPrototype) {
    variant TripLog {
      expression {
        // The "value" parameter contains the list of recorded trips
        Trip[] triplist = value;

        if (triplist != null) {
          // Get only the non-canceled trips from the list
          triplist = triplist[!trpcanceled];

          if (triplist) {
            // Get the departure date from the oldest recorded trip and save it in the
            // "trlfrom" property
            cooobj.trlfrom = triplist[0].trpdepartureat;

            // Get the arrival date from the most recently recorded trip and save it in
            // the "trluntil" property
            cooobj.trluntil = triplist[-1].trparrivalat;
          }
        }
      }
    }
  }
}

Customizing the app dashboard and app configurationPermanent link for this heading

Now that our main use cases have been implemented, let’s take a look at the app dashboard, which provides the central point of entry to your Cloud App.

The app dashboard is automatically added to the home screen of a licensed user of your Cloud App.

Our task now is to customize the app dashboard to display the list of logbooks accessible by a user in a widget. Using the WidgetView form binding trigger we will specify a custom form with a form page showing the accessible logbooks in the app dashboard widget. In addition, we will also add a desk form that is displayed when the user opens the app dashboard as well as some default column settings for the list of logbooks.

For convenience reasons, it should be possible for authorized users to create new logbooks from the dashboard. Therefore, we will implement the CreateLogbook menu use case that has to create a new logbook in the app configuration, and add it to the task pane of our app dashboard.

Finally, we have to define a desk form for the app configuration that shows the list of logbooks associated with the app configuration.

The following example demonstrates how we can accomplish all of these tasks. For brevity, we will not include the translations in the mlnames.lang files in the example.

Example

model.ducx-om

objmodel FSCLOGBOOK@111.100
{
  import COOSYSTEM@1.1;
  import FSCTERM@1.1001;

  …

  /**
   * Logbook Dashboard

   */

  class LogbookDashboard : AppDashboard {
    compapps = { AppFSCLOGBOOK }
    // Add a calculated list that retrieves the accessible logbooks
    // from the app configuration
    unique Logbook[] ldlogbooks readonly volatile(tx) {
      get = AttrLogbooksGet;
      copy = NoOperation;
    }
  }

  …
}

usecases.ducx-uc

usecases FSCLOGBOOK@111.100
{
  import COOSYSTEM@1.1;
  import COOATTREDIT@1.1;
  import COODESK@1.1;
  import FSCVAPP@1.1001;
  import FSCVENV@1.1001;
  import FSCVIEW@1.1001;

  …

  AttrLogbooksGet(parameters as AttrGetPrototype) {
    variant LogbookDashboard {
      expression {
        // Return the logbook objects accessible by the
       // current user
        value = cooobj.dbapprooms[IsUsable(#Logbook)];
      }
    }
  }

  /**
   * Creates a new logbook

   */

  menu usecase CreateLogbook direct {
    symbol = SymbolNew;
    variant LogbookDashboard {
      application {
        Logbook logbook;
        expression {
          // Create a new logbook in the app configuration and add it to the
          // list of recently used objects
          ->CreateObjectApp(cooobj.dbconfig[0], sys_action, #acapprooms, , ,
            &logbook, , , , true);
          if (logbook) {
            logbook.AddRecentlyUsed(#COODESK@1.1:CreateObject);
          }
        }
      }
      hints = {
        MH_NEEDSCURRENTVERSION
      }
    }
  }
}

dashboard.ducx-ui

userinterface FSCLOGBOOK@111.100
{
  import COOSYSTEM@1.1;
  import COOATTREDIT@1.1;
  import COODESK@1.1;
  import COOSEARCH@1.1;
  import FSCTEAMROOM@1.1001;
  import FSCVAPP@1.1001;

  …

  form DeskFormLogbookDashboard {
    // Do not display form pages inherited from the
    // FSCTEAMROOM@1.1001:AppDashboard base class
    inherit = false;
    formpage DeskPageLogbookDashboardLogbooks {
      dataset {
        ldlogbooks;
      }
    }
  }

  form WidgetFormLogbookDashboard {
    inherit = false;
    formpage WidgetPageLogbookDashboardLogbooks {
      // Set the widget size to "Medium (2x2)" so the list of available
      // logbooks is already displayed on the home screen
      formpagewidgetdimension = WD_MEDIUM;
      dataset {
        ldlogbooks;
      }
    }
  }

  forms for LogbookDashboard {
    ExploreObject { DeskFormLogbookDashboard }
    WidgetView    { WidgetFormLogbookDashboard }
  }

  columns for LogbookDashboard {
    ldlogbooks {
      objname;
      logvehicleid;
      logdescription;
      sortby { objname; }
    }
  }

  menus for LogbookDashboard {
    TaskPaneExpansion {
      CreateLogbook {
        condition = expression {
          cooobj.dbconfig[0].CheckSetAccess(cootx, #acapprooms);
        }
      }
    }
  }
}

configuration.ducx-ui

userinterface FSCLOGBOOK@111.100
{
  import COOSYSTEM@1.1;
  import COOATTREDIT@1.1;
  import COODESK@1.1;
  import COOSEARCH@1.1;
  import FSCTEAMROOM@1.1001;
  import FSCVAPP@1.1001;

  …

  form DeskFormLogbookConfigurationRoom {
    formpage DeskPageLogbookConfigurationRoom {
      dataset {
        acapprooms;
      }
    }
  }

  forms for LogbookConfigurationRoom {
    default       { FormLogbookConfigurationRoom }
    ExploreObject { DeskFormLogbookConfigurationRoom }
  }
}

Embedding visualizationsPermanent link for this heading

Let’s add some color to your Cloud App by integrating a chart!

Fabasoft Cloud makes it easy to integrate fancy charts in your Cloud App using the Highcharts library.

For a complete list of the available types of charts and additional documentation refer to http://www.highcharts.com.

To integrate a chart, you have to carry out the following steps:

  • Add a reference to the FSCHIGHCHARTS@1.1001 software component to your Cloud App project.
  • Define a data structure that corresponds to the data required by the chart you want to integrate. For instance, a bar chart requires different data than a time line or a gauge chart.
  • Define a property for providing the data to be displayed in the chart and assign it to an object class.
  • Implement a get action for calculating the data and assign it to the property.
  • Include the property on a form page and assign your visual to the property.

For our example, we will integrate a line chart showing the mileage information for every recorded trip of a trip log.

So here’s the detailed list of steps needed to integrate the chart into your Cloud App:

  • Add a reference to the FSCHIGHCHARTS@1.1001 software component to your Cloud App project.
  • Using the Highcharts documentation, we can find out that for a time line chart the first column is of type datetime, and specifies the X value on the chart and the Y value is a number that describes the corresponding date and time.
  • In the model.ducx-om file, define a data structure with the reference TimeLine that corresponds to the requirements obtained from the Highcharts documentation and is comprised of a datetime property with the reference tltripdate and a float property with the reference tltripmileage.
  • As our chart should be part of a trip log, we add a property of type TimeLine[] to the TripLog object class and assign it the reference trltimeline. The property should only be calculated on demand and no values should be stored in it. Therefore, we add the readonly property modifier suffix (see [Faba19a]).
  • In the usecases.ducx-uc file, we define and implement a get action with the reference AttrTimeLineGet for calculating the data for the chart. Afterwards, we assign the AttrTimeLineGet get action to the trltimeline property in the model.ducx-om file using the get keyword.
  • Finally, we define a new form page with the reference PageTimeLine as part of the FormTripLog form in the triplog.ducx-ui file. In the dataset block, reference the trltimeline property and use the form designer to place the trltimeline property on the form page. Then assign the FSCHIGHCHARTS@1.1001 control to the trltimeline property by selecting the property on the form designer canvas, right-clicking it to open the context menu and selecting the FSCHIGHCHARTS@1.1001:CTRLHighcharts control. Parameters such as the desired chart types are provided by returning a dictionary in the controloptions expression.

Figure 40: Using a chart to display mileage information

The following example documents all the required steps to integrate the time line chart in your Cloud App.

Example

model.ducx-om

objmodel FSCLOGBOOK@111.100
{
  import COOSYSTEM@1.1;
  import FSCTERM@1.1001;

  …

  struct TimeLine {
    datetime tltripdate;
    unsigned float(6,1) tltripmileage;
  }

  class TripLog : CompoundObject {
    …

    TimeLine[] trltimeline readonly {
      get = AttrTimeLineGet;
      copy = NoOperation;
      // Display time line chart only if there is chart data available
      visible = expression {
        !!cooobj.trltimeline;
      }
    }
  }

  …
}

usecases.ducx-uc

usecases FSCLOGBOOK@111.100
{
  import COOSYSTEM@1.1;
  import COOATTREDIT@1.1;
  import COODESK@1.1;
  import FSCVAPP@1.1001;
  import FSCVENV@1.1001;
  import FSCVIEW@1.1001;

  …

  AttrTimeLineGet(parameters as AttrGetPrototype) {
    variant TripLog {
      expression {
        // Define a variable for building the time line data
        TimeLine[] timeline;

        // Iterate over all the non-canceled recorded trips
        for (Trip trip : cooobj.trltrips[!trpcanceled]) {
          // Add an entry to the "timeline" list and populate it with the date and
          // time of departure and the trip mileage of the current trip
          timeline += {
            tltripdate: trip.trpdepartureat,
            tltripmileage: trip.trpmileage
          };
        }

        // Return the time line data in the "value" variable
        value = timeline;
      }
    }
  }
}

triplog.ducx-ui

userinterface FSCLOGBOOK@111.100
{
  import COOSYSTEM@1.1;
  import COOATTREDIT@1.1;
  import COODESK@1.1;
  import COOSEARCH@1.1;
  import FSCVAPP@1.1001;

  form FormTripLog {
    formpage PageTripLog {
      …
    }
    formpage PageTimeLine {
      symbol = SymbolChart;
      dataset {
        trltimeline;
      }
      layout {
        // Auto-generated layout block
        row {
          FSCHIGHCHARTS@1.1001:CTRLHighcharts
            FSCLOGBOOK@111.100:trltimeline {
              controloptions = expression {
                return {
                  chart: {
                    type: "line"
                  }
                }
              }
            }
          }
        }
      }
    }
  }

  …

}

In addition to the Highcharts library, Fabasoft Cloud also supports the jQuery JavaScript library, which allows you to build and integrate sophisticated custom controls for creating fantastic user experiences such as management dashboards.

Elaborating on the jQuery JavaScript library goes beyond the scope of this book. For further information on the jQuery JavaScript library refer to http://www.jquery.com and http://help.appducx.com.

The finishing touchesPermanent link for this heading

So far, so good! We’ve come a long way already, but there are still a few flaws in your Cloud App that we need to fix in order to make it perfect.

In this chapter, we’ll give your Cloud App the finishing touches before we start with the testing phase.

Defining a description for your Cloud AppPermanent link for this heading

Every Cloud App needs a brief description in form of a short sentence summarizing its purpose and key features. The description should serve as a teaser and is also displayed in the list of available Cloud Apps of a user.

The description is defined in the AppFSCLOGBOOK.appdescriptionstr entry of the mlnames.lang file and is limited to 254 characters. You may use simple HTML tags in the description string for formatting purposes or to include a hyperlink pointing to your website.

Note: If you want to define a longer description text (exceeding 254 characters) for your Cloud App, you have to provide a list of language-specific contents for the appdescription property of your app object instead.

Example

mlnames.lang

AppFSCLOGBOOK.appdescriptionstr   The Cloud App <B>Driver's Logbook</B> allows you
                                  to manage driver's logbooks for recording trip
                                  records.

Defining a name build for the ‘TripLog’ object classPermanent link for this heading

In many situations, you want the name of the instances of a particular object class to be built automatically based on rules so that users don’t have to manually assign a name.

For our trip logs, we will define a name build that automatically generates a name for each instance that is comprised of the values of the trllogbook, trlfrom, trluntil and trlstate properties.

A generated name for a trip log should look like this example:

Open Trip Log for “My Logbook” (May 1, 2019 – Apr 30, 2020)

To implement this requirement, we have to carry out the following steps:

  • Define a NameBuild customization for building the name of a trip log
  • Define pattern strings for formatting the name of a trip log

Note: All customizations must be defined in a Fabasoft app.ducx customization file with a .ducx-cu extension. For further information on customization points and customizations refer to [Faba19a].

The following example demonstrates how to implement the name build for trip logs described before.

Example

customizations.ducx-cu

customization FSCLOGBOOK@111.100
{
  import COOSYSTEM@1.1;
  import FSCCONFIG@1.1001;

  default {
    customize NameBuild<TripLog> {
      // Reference all properties that have an impact on the generated name of the
      // object in the "properties" block
      properties = {
        trlfrom,
        trluntil,
        trllogbook,
        trlstate
      }
      // The "build" expression must yield a string that is set to the new name of
      // the trip log

      build = expression {
        // Get the date of the first and last recorded trip from the trip log
        datetime from = cooobj.trlfrom;
        datetime until = cooobj.trluntil;

        if (from && until) {
          // If the trip log already contains recorded trips, the name should be
          // generated according to the pattern defined in the
          // "StrTripLogNameFormatLong" string
          #StrTripLogNameFormatLong.Print(, cooobj.Format(cooobj.trlstate),
            cooobj.trllogbook.GetName(), cooobj.Format(from, "d"),
            cooobj.Format(until, "d"));
        }
        else {
          // If the trip log does not contain any recorded trips yet, the name should
          // be generated according to the pattern defined in the
          // "StrTripLogNameFormatShort" string
          #StrTripLogNameFormatShort.Print(, cooobj.Format(cooobj.trlstate),
            cooobj.trllogbook.GetName());
        }
      }
    }
  }
}

strings.ducx-rs

resources FSCLOGBOOK@111.100
{
  import COOSYSTEM@1.1;
  import COODESK@1.1;

  …

  string StrTripLogNameFormatShort;
  string StrTripLogNameFormatLong;
}

mlnames.lang

StrTripLogNameFormatLong.string    %1$s Trip Log for "%2$s" (%3$s - %4$s)
StrTripLogNameFormatShort.string   %1$s Trip Log for "%2$s"

Ensuring that only a single open trip log may exist in a logbookPermanent link for this heading

It doesn’t make sense to have more than one open trip log in a logbook at any given time. Therefore, we’ll enforce this business rule by augmenting our implementation of the object constructor of the TripLog object class a bit.

Example

overrides.ducx-uc

usecases FSCLOGBOOK@111.100
{
  import COOSYSTEM@1.1;
  import FSCVAPP@1.1001;
  import FSCVENV@1.1001;
  import FSCVIEW@1.1001;
  import COODESK@1.1;

  override ObjectConstructor {
    variant TripLog {
      expression {
        super();

        Logbook logbook = cooobj.GetObjectRoom();

        if (logbook.IsUsable(#Logbook)) {
          // Check if there is another trip log in "open" state in the logbook already
          if (logbook.logtriplogs[trlstate == TLS_OPEN]) {
            // Throw an error if the logbook already contains an open trip log
            throw coort.SetError(
              #ErrOpenTripLogFound,
              #ErrOpenTripLogFound.Print(, parent.GetName()));
          }

        }
      }
    }
  }
}

errors.ducx-rs

resources FSCLOGBOOK@111.100
{
  import COOSYSTEM@1.1;
  import COODESK@1.1;

  …

  errormsg ErrOpenTripLogFound;
}

mlnames.lang

ErrOpenTripLogFound.errtext     Logbook "%1$s" already contains an open trip log,
                                which must be closed before new trip logs can be
                                created.

Picking an app categoryPermanent link for this heading

An app category allows you to combine the object classes belonging to your Cloud App into a distinct category that is displayed when a user is creating a new object.

You can either assign your object classes to an existing app category or define a new app category for your Cloud App if the existing app categories don’t seem to be a good fit.

Name

Reference

Application Integration

COOTC@1.1001:CategoryApplicationIntegration

Collaboration

FSCFOLIOCLOUDAPPS@1.1001:CategoryCollaboration

Development

COOTC@1.1001:CategoryDevelopment

Family

FSCFOLIOCLOUDFAMILY@1.1001:CategoryFamily

Finance

COOTC@1.1001:CategoryFinance

Games

COOTC@1.1001:CategoryGames

Multimedia

FSCFOLIOCLOUDAPPS@1.1001:CategoryMultimedia

Productivity

COOTC@1.1001:CategoryProductivity

Tools

COOTC@1.1001:CategoryTools

Table 3: Available app categories

For your Cloud App, we will assign all of your object classes to the existing app category “Finance”. To do so, add a reference to the COOTC@1.1001 software component and use the extend instance keyword to extend the app category CategoryFinance in the app.ducx-om file and add the object classes belonging to your Cloud App to the templates list of the app category.

Example

app.ducx-om

objmodel FSCLOGBOOK@111.100
{
  import COOSYSTEM@1.1;
  import COODESK@1.1;
  import COOATTREDIT@1.1;
  import COOTC@1.1001;

  instance App AppFSCLOGBOOK {
    …
  }

  extend instance CategoryFinance {
    templates = {
      Logbook,
      TripLog
    }
  }
}

Defining the context-sensitive helpPermanent link for this heading

At the beginning of the object modeling section, we mentioned that you are required to document your source code using Javadoc style comments.

Unfortunately, that’s not all of the documentation work you have to do as a Cloud App developer.

At Fabasoft, we strongly believe that a solid Cloud App has to come with solid user documentation in order to deliver an exceptional user experience, and this is why we also require you to provide context-sensitive help for your Cloud App.

Users can easily turn the context-sensitive help on and off by a simple click on the “?” button available in all dialogs (which is visible unless it has been explicitly deactivated for a dialog).

So how can you provide context-sensitive help for your Cloud App?

You may have already noticed the explanations.userdoc files underneath the language-specific folders in the resources folder of your Cloud App project. This is where you have to define the context-sensitive help.

Basically, an explanations.userdoc file is content in XML format comprising an explanation node for each element of your Cloud App that needs to be included in the context-sensitive help. The Fabasoft app.ducx compiler automatically updates the files when you add new properties and other elements to your Cloud App.

All you need to do is replace the “Your content goes here!” placeholders with the actual help text.

Be aware that you have to obey XML formatting and escaping rules when modifying the explanations.userdoc files.

Example

explanations.userdoc

<?xml version="1.0" encoding="UTF-8"?>
<explanations xmlns="http://www.fabasoft.com/ducx/explain/20090309#">
  <explanation reference="trllogbook" type="detail">
    <content>
      <b>Logbook</b>
      <p>This field displays the logbook this trip log belongs to.</p>
    </content>
  </explanation>
  <explanation reference="trlstate" type="detail">
    <content>
      <b>State</b>
      <p>This field displays the current state of the trip log.</p>
    </content>
  </explanation>
  <explanation reference="trluntil" type="detail">
    <content>
      <b>Last Trip on</b>
      <p>This field displays the arrival date of the last trip recorded in this trip.
      </p>
    </content>
  </explanation>
  <explanation reference="trlfrom" type="detail">
    <content>
      <b>First Trip on</b>
      <p>This field displays the departure date of the first trip recorded
         in this trip log. </p>
    </content>
  </explanation>
  <explanation reference="trltrips" type="detail">
    <content>
      <b>Recorded Trips</b>
      <p>This field displays the list of trips recorded in this trip log.</p>
    </content>
  </explanation>
  …
</explanations>

Figure 41: Displaying context-sensitive help

Advanced stuffPermanent link for this heading

In this chapter, we will discuss some of the more advanced stuff such as the integration of web services and provide a brief overview of other APIs you can use in the context of your Cloud App.

Creating a web servicePermanent link for this heading

Creating a SOAP web service is easy! All you need to do is define and implement the web service methods, and then define a web service definition where you reference the web service methods that you want to expose as a web service.

Note: Any use case can be exposed as a web service method. You just need to make sure that it is implemented on object class COOSYSTEM@1.1:User. As a convention, all web service methods should be prefixed with SOAP. Also, make sure to call the COOATTREDIT@1.1:CheckLicense action to perform a license check in your web service methods.

In our example, we define three web service methods for creating, updating and deleting logbooks in a new Fabasoft app.ducx use case file named webservices.ducx-uc. For the sake of brevity, we will not elaborate on the actual implementation details.

We define a web service definition in the instances.ducx-om file, where we reference the use cases we want to expose as web methods of the web service. In the web service definition, we can also assign the external names of the web service methods (e.g. CreateLogbook, UpdateLogbook and DeleteLogbook).

Example

webservices.ducx-uc

usecases FSCLOGBOOK@111.100
{
  import COOSYSTEM@1.1;
  import COOATTREDIT@1.1;
  import COODESK@1.1;
  import FSCTEAMROOM@1.1001;
  import FSCVAPP@1.1001;
  import FSCVENV@1.1001;
  import FSCVIEW@1.1001;

  usecase SOAPCreateLogbook(string name, string vehicleid, retval Logbook logbook) {
    variant User {
      expression {
        #AppFSCLOGBOOK.CheckLicense();

        if (name && vehicleid) {
          LogbookConfigurationRoom config = coouser.userappconfigrooms
            [IsUsable(#LogbookConfigurationRoom)][0];
          Logbook logbook = #Logbook.ObjectCreate();
          logbook.objname = name;
          logbook.logvehicleid = vehicleid;
          logbook.arappconfiguration = config;
        }
        else {
          throw #ErrMissingInformation;
        }
      }
    }
  }

  usecase SOAPUpdateLogbook(Logbook logbook, optional string name,
    optional string vehicleid) {
    variant User {
      expression {
        #AppFSCLOGBOOK.CheckLicense();

        if (logbook != null) {
          logbook.ObjectLock(true, true);

          if (logbook.objname != name) {
            logbook.objname = name;
          }

          if (logbook.logvehicleid != vehicleid) {
            logbook.logvehicleid = vehicleid;
          }
        }
        else {
          throw #ErrMissingInformation;
        }
      }
    }
  }

  usecase SOAPDeleteLogbook(Logbook logbook) {
    variant User {
      expression {
        #AppFSCLOGBOOK.CheckLicense();

        if (logbook.IsUsable(#Logbook)) {
          logbook.ObjectDelete();
        }
        else {
          throw #ErrMissingInformation;
        }
      }
    }
  }
}

instances.ducx-om

objmodel FSCLOGBOOK@111.100
{
  import COOSYSTEM@1.1;
  import FSCTERM@1.1001;
  import FSCOWS@1.1001;
  import FSCGOOGLEVISUALS@1.1001;

  …

  instance WebServiceDefinition WebServiceLogbook {
    webserviceactions<webserviceoperation, webserviceaction> = {
      {"CreateLogbook", SOAPCreateLogbook},
      {"UpdateLogbook", SOAPUpdateLogbook},
      {"DeleteLogbook", SOAPDeleteLogbook}
    }
  }
}

errors.ducx-rs

resources FSCLOGBOOK@111.100
{
  import COOSYSTEM@1.1;
  import COODESK@1.1;

  …

  errormsg ErrMissingInformation;
}

mlnames.lang

ErrMissingInformation.errtext           You did not provide all the required data.

Invoking a web servicePermanent link for this heading

Invoking a SOAP web servicePermanent link for this heading

Using your Cloud App VDE, you can generate a web service description language (WSDL) document for your web services and web methods.

A special URL allows you to retrieve the WSDL document for a web service definition in your Cloud App VDE:

https://vde.fabasoft.com/dev<X>/vm<Y>/folio/fscdav/wsdl?WEBSVC=<web service definition>

The WEBSVC URL parameter is used to reference the web service definition. You can either specify the fully qualified reference or the address of the web service definition. If you specify the fully qualified reference, you must replace special characters (@, colons and dots) with underscores.

The following example URL generates the WSDL document for our web service definition with the reference FSCLOGBOOK@111.100:WebServiceLogbook:

https://vde.fabasoft.com/dev2/vm23/folio/fscdav/wsdl?WEBSVC=FSCLOGBOOK_111_100_WebServiceLogbook

The following figure shows part of the WSDL document generated for our web service.

Note: The URLs mentioned before may only be used for writing web service consumers that connect to your Cloud App VDE for testing purposes. Once your Cloud App has been deployed into the Fabasoft Cloud production system, use the following URL to reach out to your web service definition from a web service consumer:

https://cloud.fabasoft.com/folio/fscdav/wsdl?WEBSVC=<web service definition>

Refer to [Faba19f] for an introduction on how to create a web service consumer using either Eclipse, the Axis2 code generator and Java; Microsoft Visual Studio and C# or Eclipse; or Fabasoft app.ducx and expression language.

Figure 42: WSDL output generated for the web service

Invoking a RESTful web servicePermanent link for this heading

In addition to the SOAP web service interface, Fabasoft Cloud also provides a RESTful web service interface that allows you to invoke individual web methods of your web service definition using the following URL:

https://vde.fabasoft.com/dev<X>/vm<Y>/folio/wsjson/<web service definition reference>/<web method name>

The use cases associated with the web methods that you want to expose via the RESTful web service interface must be assigned the COOSYSTEM@1.1:DefaultSeeInWebACL ACL.

The following example URL allows you to invoke the CreateLogbook method of your FSCLOGBOOK@111.100:WebServiceLogbook web service:

https://vde.fabasoft.com/dev2/vm23/folio/wsjson/FSCLOGBOOK_111_100_WebServiceLogbook/CreateLogbook

Parameters are transported as JSON encoded strings.

The following code fragment demonstrates how to write a consumer for a RESTful web service in Fabasoft app.ducx expression language.

Example

import COOSYSTEM@1.1;
import FSCOWS@1.1001;
import FSCEXPEXT@1.1001;

content responsebody = null;
dictionary responseheaders = null;

// Create a dictionary holding the parameters to be passed into the web service

dictionary requestdict = {
  name: "Claudia's Logbook",
  vehicleid: "CLAUDIA-1"
};

// Call FSCEXPEXT@1.1001:Value2JSON to generate a JSON string from the
// parameter dictionary and store it in a content

content requestbody = {};
requestbody.SetContent(cootx, COOGC_MULTIBYTEFILE, COOGC_UTF8,
  coouser.Value2JSON(requestdict));

// Send an HTTP POST request to the RESTful web service and provide login credentials
coouser.SendHttpRequest("POST", "https://vde.fabasoft.com/dev2/vm23/folio/wsjson/
  FSCLOGBOOK_111_100_WebServiceLogbook/CreateLogbook", , requestbody,
  &responseheaders, &responsebody, "kimble0001", "password");

// Process the response received from the web service
dictionary responsedict = coouser.JSON2Value(responsebody.GetContent(cootx,
  COOGC_MULTIBYTEFILE, COOGC_UTF8));

%%ASSERT(responsedict.logbook.objname == requestdict.name);

Other supported APIsPermanent link for this heading

Fabasoft Cloud supports a plethora of open standard APIs such as WebDAV, CalDAV (see [IETF07] and [Faba19g]) and CMIS (see [OASI13] and [Faba19h]) to make it as simple as possible for you to integrate or consume virtually any type of external resource within your Cloud App.

Note: The software component FSCOWS@1.1001 contains useful actions for invoking SOAP web service methods (CallSoapXML, CallSoapXMLEx) as well as for sending generic HTTP requests (SendHttpRequest).

Elaborating on these technologies and APIs would go beyond the scope of this book. For further information refer to the mentioned white papers and the resources listed in the chapter “Getting help, code samples and support”.

Tracing and debuggingPermanent link for this heading

Sometimes things just don’t work out at the very first shot. That’s why we’ve included some nifty features that allow you to add trace messages and debug through your code so that you can easily and efficiently track down any runtime issues that might arise during development.

Tracing in Fabasoft app.ducx expression languagePermanent link for this heading

The built-in tracing functionality of Fabasoft app.ducx allows you to produce extensive traces of your use case implementations.

The trace output contains detailed information about all use cases invoked by your use case implementations, along with the parameter values passed to and returned by the invoked use cases to allow you to get a complete picture of the call stack.

Using Fabasoft app.ducx expression language, you can use the %%TRACE directive to write custom trace messages to the Fabasoft app.ducx Tracer.

The %%TRACE directive can also be used to trace special objects like cootx to output all transaction variables defined for a transaction or coometh to output all set parameters within the implementation of a method.

Values traced using the %%TRACE directive are only written to the Fabasoft app.ducx Tracer if trace mode is enabled for your Cloud App in the project preferences. Your trace messages, therefore, don’t have a performance impact when tracing is deactivated.

To enable tracing, select your Cloud App project in Project Explorer, open the context menu and select “Properties”. Open the “Fabasoft app.ducx” page, select Enable tracing and click “OK”.

You can view the trace output of the Fabasoft app.ducx Tracer in the “Console” view of Eclipse. To activate the “Console” view select “Window” > “Show View” > “Other”, then select “Console” from the “General” branch and click “OK”.

Before you can see the trace output in the “Console” view, you have to start a tracing session. To start a tracing session, click the “Start Tracing Session” button in the button bar of the “Console” view. While the tracing session is active, all trace output is retrieved from the Cloud App VDE and logged in the “Console” view of Eclipse. When you’re done, click the “Stop Tracing Session” button to stop the tracing session.

Figure 43: Viewing the trace output of the Fabasoft app.ducx Tracer in Eclipse

The following example demonstrates how to use trace directives in Fabasoft app.ducx expression code.

Example

usecases.ducx-uc

usecases FSCLOGBOOK@111.100
{
  import COOSYSTEM@1.1;
  import COOATTREDIT@1.1;
  import FSCVAPP@1.1001;
  import FSCVENV@1.1001;
  import FSCVIEW@1.1001;

  menu usecase RecordTripWizard on selected or container {
    symbol = SymbolNew;
    accexec = AccTypeChange;
    variant TripLog {
      application {
        expression {
          // Custom trace message
          %%TRACE "RecordTripWizard invoked...";

          // Write the contents of the "cooobj" variable to the tracer
          %%TRACE("Trip Log", cooobj);

          if (cooobj.trlstate == TLS_OPEN) {
            cooobj.InitTrip();
            ->DialogRecordTrip;
          }
          else {
            throw coort.SetError(
              #ErrTripLogClosed,
              #ErrTripLogClosed.Print(, cooobj.GetName()));
          }
        }

        …
      }
    }
  }
}

For detailed information on tracing refer to [Faba19a].

Debugging your Cloud AppPermanent link for this heading

The Fabasoft app.ducx debugger allows you to include breakpoints in your code and step through your use case implementations from within the Eclipse IDE.

You can set a breakpoint by double clicking in the column just in front of your app.ducx source code or by using the %%DEBUGGER directive in your expression code.

Refer to [Faba19a] to learn how to configure and activate the Fabasoft app.ducx debugger.

Testing expressionsPermanent link for this heading

The Fabasoft app.ducx Expression Tester is a pretty handy tool that allows you to test expressions on the fly by evaluating them in context of your Cloud Sandbox at the click of a button.

You can select any expression in your source code, open the context menu and select “Run As” > “Run app.ducx Expression” or simply press Alt+X to execute the selected code. The result is shown in the “Console” view of Eclipse.

Alternatively, you can also create a new Fabasoft app.ducx expression file (with a .ducx-xp extension), write your expressions and then press Alt+X to execute them.

Figure 44: Fabasoft app.ducx Expression Tester

Refer to [Faba19a] for further information about the Fabasoft app.ducx Expression Tester.