# Accessing BO Descriptors

reviewed: 10 March 2025

Writing BO-Savvy Code

We have learned the term self-describing business objects. It is the self-describing feature that makes it possible to develop highly generic components. These components are sometimes referred to a being BO-savvy. They can work with any self-describing business object; one generic component that can be used by countless number of different business objects.

Component BO Result
List Client List of Clients
Order List of Orders
PrintRequest List of PrintRequests
Form Flight Form to view / edit details of Flights
Transaction Form to view / edit details of Transactions
Visit Form to view / edit details of Visits

The boDesc Function Handler

The boDesc function handler is designed to read and set BO descriptor values.

We start by looking at the functions to read value; see the paragraph on massaging descriptors for information about the functions to set values.

The following example shows two similar functions; one to get the label of the BO and one to get the label of a specific attribute.

logError( boDesc.getLabel( [_me] ) )
logError( boDesc.getAttrLabel( [_me], 'id' ) )

These functions will return the values as can be found in the entity script file:

    <@bo
        label: 'Client'             // Returned by boDesc.getLabel( [_me] )
        table: 'client'
        primaryKey: 'id'
        sequence: 'client'
        auditable: auditing.Auditable
        deleteRule: deleteRule.PerRelation
        sequenceStep: 2
        attributes: <
            id: <@bo/attribute
                label: 'Id'         // Returned by boDesc.getAttrLabel( [_me], 'id' )
                column: 'id'
                dataType: dataType.Automatic
                length: 9
                locked: true()
            >

Most functions are pretty straightforward and map more or less directly on the BO descriptor settings.

A number of functions are worth a separate mention.

boDesc.inGroup, boDesc.anyInGroup

Test whether one or more attributes exist in a group. Often used in _eventAction methods.

    if and(
        eq( [timing], eventActionTiming.Pre ),
        in( [type], eventActionType.Insert, eventActionType.Update ),
        // Is accessCode in the attribute group?
        boDesc.inGroup( [_me], 'accessCode', [group] ),
        ne( left( bo.attr( [_me], 'accessCode' ), 1 ), '+' )
    )
        raise exceptionType.soft, 'Access code must start with +'
    end-if

Private Descriptors boDesc.create, boDesc.isPrivate

Descriptors are cached for performance reasons. When you do a bo.create( 'client' ), CaseMaster will check whether the descriptor for client is already in the cache. When it is, the descriptor is retrieved from cache, otherwise client.cms is loaded for the first time, parsed and a new descriptor is created from it which is then added to the cache.

It can sometimes be useful to have a private descriptor; one that is not added to the cache and is thus never shared between different instantiated business objects.

A common scenario is where you massage the descriptor (see that paragraph) and you do not want your temporary descriptor overrides to be shared.

// Get a private descriptor
set( 'privateDescriptor', boDesc.create( 'client' ) )

// Massage the private descriptor here
set( 'bo', bo.create( [privateDescriptor] ) )

The following example shows how to get a handle to the cached descriptor:

set( 'descriptorFromCache', boDesc.get( bo.create( 'client' ) ) )

Descriptor Alias, BO Alias: getAlias and setAlias

An alias of a descriptor or business object can be used to ensure that CaseMaster generates correct SQL in joins (see here) or when resolving foreign keys (see here).

The scenarios are typically highly advanced and often originate from the same entity included in a join more than once.

You can set the alias on the entity by including the alias: 'something' tag in the descriptor .cms file. This means the alias is always set.

You can also set the alias using the boDesc.setAlias() function. See the paragraph on massaging descriptors.

Where possible, you can also set the alias on a business object instead of its' descriptor. The performance will be better as business objects are thread safe by default. Use the bo.setAlias() function for this.

See also:

  • The foreignKeyAlias for foreign key attributes (see here)
  • The alias in sub-selects in where clauses (see here)

boDesc.serialize

Use the boDesc.serialize() function to dump the .cms code for the <@bo> qualifier of a descriptor.

// Can work on an entity name or business object
logError( boDesc.serialize( 'client' ) )

logError( boDesc.serialize( bo.create( 'client' ) ) )

The iterator.ofAttribute() Function

The iterator.ofAttribute() function returns an iterator with information about attributes in a group of a business object.

The function takes 3 parameters; a business object or entity name, an attribute group and the variable name that you can refer to inside the iterate - end-iterate construct.

// Use with entity name
iterate iterator.ofAttribute( 'client', 'description', 'attr' )
    response.write( [attr] )
    response.write( '<br>' )
end-iterate

// Use with BO
set( 'cl', bo.create( 'client' ) )
iterate iterator.ofAttribute( [cl], 'description', 'attr' )
    response.write( [attr] )
    response.write( '<br>' )
end-iterate

The iterator consists of zero or more property bags that have the following format:

<
    name: "firstname"
    bodescriptor: "client"
>

Often, you (ab)use the feature of a property bag that when you treat it as a string it simply returns the first value. See our example above.

Putting it Together

Putting it all together

The following code shows how the boDesc function handler and the iterator.ofAttributes() function can be used to create a generic routine to dump values in a HTML table:

// 1. Change client to user, user/session or any other valid entity name
// set( 'entity', 'user' )
// set( 'entity', 'user/session' )
set( 'entity', 'client' )

// 2. Open container, table and thead
response.write( '<div class="container">')
response.write( '<table class="table table-sm table-striped table-bordered">')
response.write( '<thead class="bg-primary text-light">')

// 3. Iterate over the attributes in the descriptor attribute group
iterate iterator.ofAttribute( [entity], 'description', 'attr' )
    // 4. Write td with the label of the attribute
    response.write( formatString( '<td>{0}</td>', boDesc.getAttrLabel( [entity], [attr] ) ) )
end-iterate

// 5. Close the thead and open the tbody
response.write( '</thead>')
response.write( '<tbody>')

// 6. Iterate over all rows of the entity
iterate iterator.ofEntity( < < name: 'BO' entity: [entity] > > )
    // 7. Open tr
    response.write( '<tr>')

    // 8. Iterate over the attributes in the descriptor attribute group; note that we could also have used [entity] instead of [BO]
    iterate iterator.ofAttribute( [BO], 'description', 'attr' )
        // 9. Write td with the value of the attribute
        response.write( formatString( '<td>{0}</td>', bo.attrFormatted( [BO], [attr] ) ) )
    end-iterate

    // 9. Close tr
    response.write( '</tr>')
end-iterate

// 10. Close the tbody, table and container div
response.write( '</tbody>')
response.write( '</table>')
response.write( '</div>')

Massaging Descriptors

Sometimes it can be useful to change the behaviour of a business object at runtime. Examples:

  • An attribute that is optional needs to be mandatory on a specific form
  • In a specific list the column header for an attribute needs a different label

This can be done by massaging the descriptor. You can safely set / update descriptor settings using the boDesc function handler.

There is a (small) performance penalty as CaseMaster will create a private clone of the descriptor so the alterations are not shared.

    // Massage the descriptor so the client is locked
    boDesc.setAttrLocked( [order], group: 'client', value: true() )

See the CaseMaster Playground for all functions in the boDesc handler.

Notes on the boDesc Function Handler

  • Most functions in the boDesc handler can be applied to a BO object or a descriptor object
  • Some can also apply to an entity name, but none of them allow you to set anything
  • When applied to a BO object, the changes only apply to that BO, not to other BOs of the same entity
  • When applied to a descriptor, the changes apply to all BOs created from that descriptor after the change has been applied

BO and Attribute Enhancers

Enhancers are designed to enhance the behaviour a business object or attribute. Often, enhancing is used to enhance the look & feel on an HTML page.

There are two enhancers for a BO:

Enhancer Usage
noAudits States whether or not audits should be written
noTouch States whether or not audit attributes should be written

Both are explained in detail here.

The following shows how this could be included in an entity '.cms' file although it is more often that these enhancers are set at runtime.

resource main
    <@bo
        label: 'Client'
        table: 'client'
        primaryKey: 'id'
        sequence: 'client'
        auditable: auditing.Auditable
        deleteRule: deleteRule.PerRelation
        sequenceStep: 2
        enhancers: <@bo/enhancers
            noAudits: true()
        >

As explained, the BO enhancers are typically set at runtime. Setting an enhancer at runtime will also force the creating of a private descriptor.

    set( 'cl', bo.create( 'client' ) )
    boDesc.setEnhancer( [cl], 'noAudits', true() )

There are many more enhancers for attributes. A full list can be found by reading the source of the qualifier <@bo/attribute/property/enhancers>.

Enhancer Usage Example
disabled States whether or not an attributes is disabled false()
noLabel States whether or not an attributes label should be visible false()
labelClass Specifies one or more class names for the attributes form label 'expressionClass'
labelTooltip Specifies the tooltip for the attributes form label 'Enter a positive number'
inputClass Specifies one or more class names for the attributes form input 'mandatoryClass'
inputTooltip Specifies the tooltip for the attributes form input 'Enter a positive number'
inputDir Specifies the text direction of the attributes form input ltr
inputPattern Specifies a regular expression that the attributes form input value is checked against on form submission '999[a-z]'
inputTitle Specifies extra information about the attributes form input, most often shown as a tooltip 'Enter a positive number'
inputStep Specifies the interval between legal numbers in an input element 0.1
inputType Specifies a type override to allow presenting an input in a nonstandard way 'url'
lockedClass Specifies one or more class names for the attributes locked input 'lockedClass'
lockedTooltip Specifies the tooltip for the attributes locked form value 'You do not have the access rights to edit the amount'
headerClass Specifies one or more class names for the attributes table header 'headerClass'
headerTooltip Specifies the tooltip for the attributes table header 'Click to sort'
cellClass Specifies one or more class names for the attributes table cell 'cellClass'
cellLink Specifies one or more class names for the attributes table cell See further
cellTooltip Specifies the tooltip for the attributes table cell 'Click to view document'
prependInput Specifies a collection of elements to prepend to the attributes from input -
appendInput Specifies a collection of elements to append to the attributes from input See further
preInput Specifies a collection of elements to render before the attributes from input -
postInput Specifies a collection of elements to render after the attributes from input See further
multiLineRows Specifies the number of rows to display for multiline text inputs 12
selectSorter Specifies the sorter JavaScript callback for option list select inputs alphabetical to sort alphabetical
foreignKeyRows Specifies the number of rows to fetch for each foreign Key select input request 50

The following is a real-life example of the cellLink enhancer:

    _listDocument: <@bo/attribute/dynamic
        dynamicValue: $if(
            isNotNull( bo.attr( [_me], 'document' ) ),
            '<span class= "btn btn-outline-primary btn-sm">View document...</span>',
            null()
        )
        htmlSafe: true()
        enhancers: <@bo/attribute/property/enhancers
            cellLink: <@page/table/link
                url: <@url
                    address: $script.call('web/router/service:resolveUrl', 'document/view/')
                    untrustedQs: <
                        context: $encrypt( bo.attr( [_me], 'document' ) )
                    >
                >
                target: 'tab'
            >
        >
    >

The following is a real-life of the appendInput enhancer:

    appendInput: <@page/input/group/append
        excluded: $or(
            // The the business object has not been loaded (i.e. create instead of edit)
            ne(bo.attrPersistStatus([__bo], boDesc.getPK([__bo])), attrPersistStatus.Loaded),
            // The attribute is disabled (i.e. can not edit)
            boDesc.attrIsDisabled([__bo], [__attribute])
        )
        content: <@page/button
            class: 'btn-light border'
            icon: <@page/icon name: 'language'>
            target: <@page/link/target/modal
                size: 'xl'
                title: $getTranslation('multilanguage/translations')
            >
            url: <@url
                address: 'application/multilanguage/edit:main'
                persistQs: false(),
                qs: <
                    pk: $bo.pk([__bo])
                    bo: $boDesc.name([__bo])
                    attribute: $[__attribute]
                    control: $boDesc.getControlName([__bo], [__attribute])
                >
            >
        >
    >

The following is a real-life of the postInput enhancer:

    group: <@bo/attribute/memo
        label: <@text
            EN: "Group"
        >,
        datatype: datatype.String
        enhancers: <@bo/enhancers
            postInput: <@page/text/small
                class: 'text-muted',
                content: 'Relevant for update only'
            >
        >
    >

<End of document>