Skip to Content
Technical Articles

Ways to provide and bind codelist descriptions to any mashup

In my last openSAP training about developing on SAP Business ByDesign, I’ve being looking for ways to provide the codelist description field as inport parameter of a mashup.

Let’s define a use case requirement to focus in context. The typical how to show a country location in an embed Google Maps iframe.

 

Binding data is retrieved from a CountryCode data type stored in our business object (BO).

import AP.Common.GDT as apCommonGDT;
import AP.PDI.bo as pdi;

businessobject MyCustomBO
{
	....

	[Label("Country")]
	element country: CountryCode;

	....
}

CountryCode is a codelist type. Are based on string XSD basic type and it value is the country ISO code, and that it’s exactly the value that is provided to mashup. Eg.: “FR” for France, “ES” for Spain…

Since we need it description property translated into language of the user that is logged in at the moment to pass it as query value to google maps, we have to make some coding.

 

Also assume that we have already created in our add-on the corresponding:

  • Port Type Package (PTP) file, e.g. /Mashups/Mashups.PTP.uicomponent, with a port type named Inport and a single property in it named Address.
  • Mashup Port Binding (PB), e.g. MashupPB.PTP.uicomponent, in category Location & Travel and an Inport reference to that PTP port type.
  • An HTML Mashup using that PB and with the following code that allows us to render the map in the current user language:
<!DOCTYPE html>
<html>
<head>
<title>Simple embedded map</title>
</head>
<body>
<script type="text/javascript">
(function(w, d, l, sap){
  sap.byd.ui.mashup.onContextUpdate = function() {
    // mashup context parameters
    var ctx = sap.byd.ui.mashup.context;
    var lang = ctx.LogonLanguage;
    // render map
    loadmap({hl: lang, q: ctx && ctx.inport && ctx.inport.Address});
  };
  
  var iframe;
  function loadmap(opts) {
    if (!iframe) { // generate a singleton iframe
      iframe = d.createElement("iframe");
      iframe.frameBorder = opts.border || 0;
      iframe.id = "googleframe-" + (1 * new Date());
      iframe.height = opts.height || 500;
      iframe.width = opts.width || 500;
      d.body.appendChild(iframe);
    }
    // compose url using options
    var url = "//maps.google.com/?output=embed";
    opts.hl && (url += "&hl=" + encodeURIComponent(opts.hl));
    opts.q && (url += "&q=" + encodeURIComponent(opts.q));
    // change iframe url to trigger browser refresh
    iframe.src = url;
  }
  
  w.onload = function() {
    // fire on load mashup iframe container
    sap.byd.ui.mashup.onContextUpdate();
  };
}(window, document, location, sap));
</script>
</body>
</html>

 

Taking a look at the Cloud Application Studio 1911 documentation, there are some facilities to implement it.

 

 

Using front-end scripts

This is the fast way if we only want to expose the description to floorplans. Datasources, webservices, forms and other objects are not allowed here.

Also if we are extending a standard BO floorplan to add the mashup, we can not get it directly either. Due to extensibility restrictions, basically we can’t create Event Handlers of any type.

To bypass this, we could use built-in functions and declaring other field in the BO.

An alternative at this point, of course from the point of view of clean code paradigm, to coding the less as posible and valid for both, custom and standard BOs, would be move the description resolution to the mashup source code. To do this, we need use data mashup webservices and for example invoke the standard SOAP service QueryCodeListIn in it.

Another option to coding the less as posible in each floorplan we want to add the map, and maybe the best, is create an Embedded Component (EC) that contains our HTML mashup and acts like a proxy in charge of doing all previous transformation hard work that I’m going to explain below. Take an idea of the steps reading this Vitina’s post.

 

Reading the Cloud Application Studio 1911 documentation

You can access the code value description by adding .Description.

So, follow these steps in the floorplan we want to handle this feature:

  1. Open the floorplan in UI Designer.
  2. In the DataModel, if not yet added, add a field binded to our BO codelist element:
    Name: country
    CCTS Type: code
    Binding Field: -~country
  3. In the DataModel, add a new field:
    Name: location
    CCTS Type: text
    Type: string
    Work Protect Relevant: False
  4. In the Controller define a new Event Handler to codify the transformation:
    Name: CalculateLocation
    Operations:
    1. Type: Script
    Name: CalculateLocation
    Blocking: True
    Source code:
    # Get DataModel field value
    country = $data.country
    # Get codelist description in current user language
    description = country.Description
    
    #set it in DataModel field consumed by mashup
    $data.location = description
    Scope fields: Remember add each source field used in script to calculate our value to this list. Scoped fields are hooked to refire the script again when hash of provided fields change. In our use case:

    • /Root/country
  5. In the Controller define a new OutPort to provide data to our mashup:
    Name: LocationAddressOutPort
    Port Type Package: the same PTP file that we provide to the Mashup PB: e.g. /XXXX_MAIN/SRC/Mashups/Mashups.PTP.uicomponent
    Port Type Reference: the same value of port type when we create that PTP. E.g.: Inport
    Parameters:
    1. ParameterName: Address
    ParameterBinding: /Root/location

    and remember bind Mashup and create a navigation to it mapping Outport fields to Mashup. It’s easy since has got the same PTP: LocationAddressOutPort::Address -> Inport::Address

  6. Go back to DataModel. Edit the country field added in step 2 to provide it an OnValueChange event handler. With this, when we change the value in your BYD tenant application, the mashup will be updated again inmediately.
    Name: RefreshLocation
    Operations:
    1. Type: FireEventHandler
    Name: CalculateLocation
    EventHandler: CalculateLocation
    2. Type: SyncDataContainer
    Name: RefreshData
    Finalize: Checked
    3. Type: FireOutport
    Name: SendData
    OutPort: LocationAddressOutPort

All the magic are made by steps 4 and 6.

 

 

Using built-in functions

Through this way, we can cover a larger set of casuistries and behaviors, such as exposing the description as dedicated field to UI, webservices, forms, datasources, extended BOs… And with only define a few lines, reuse our code to share the same behaviour between all that target sources. From the point of view of clean code paradigm, it sound very well.

One more time, reading the documentation, we can see that codelist types also expose built-in mechanisms to be used in your ABSL scripts:

CodeListUtilities-Function-GetCountryCodeDescription.absl
import ABSL;
import AP.Common.GDT as apCommonGDT;
import AP.PDI.bo as pdi;
import BASIS.Global as basis;

var result : DataType::pdi:String;

//
// Import parameters
//
var countryCode : apCommonGDT:CountryCode = Parameter::countryCode;
var otherLanguage : basis:LanguageCode = Parameter::otherLanguage;

//
// If other language is provided, use it
//
if (otherLanguage.IsInitial() || otherLanguage == Context.GetCurrentUserLanguage())
{
	result = countryCode.GetDescription();
}
else
{
	result = countryCode.GetDescriptionInOtherLanguage(otherLanguage);
}

return result;

Call example:

var descriptionES = Library:CodeListUtilities.GetCountryDescription("FR", LanguageCode.ParseFromString("ES"));
Trace.Info(description); // => "Francia"

var descriptionEN = Library:CodeListUtilities.GetCountryDescription("FR");
Trace.Info(description); // => "France"

Also, there are a built-in function in PlatinumEngineering library to abstract it from any codelist but with a cost. We have to pass their namespace and name as parameters and later, iterate each one looking for the value to get it description field.

CodeListUtilities-Function-GetDescription.absl
import ABSL;
import AP.PDI.bo as pdi;
import AP.PlatinumEngineering as apPE;

var result : DataType::String;

//
// Import parameters
//
var codelistNamespace = Parameter::codelistNamespace; //pdi:String
var codelistName = Parameter::codelistName; //pdi:String
var codeValue = Parameter::codeValue; //pdi:String

//
// Get CodeList values dinamically using Platinum libraries
//
var codelist = Library::apPE:Codelist.Get(codelistName, codelistNamespace);
if (codelist.Count() == 0)
{
	Trace.Error(
		"Codelist Not found // Not valued. Revise provided Name and Namespace.",
		codelistNamespace + " :: " + codelistName
	);
	return result;
}

var found = false;

foreach (var code in codelist)
{
	var value = code.CodeValue;
	var desc = code.Description;
	var descContent = desc.content;
	var descLang = desc.languageCode;
	if (value == codeValue)
	{
		found = true;
		result = descContent;
		break;
	}
}

if (!found) {
	Trace.Info(
		"Codelist value description not found.",
		codelistNamespace + " :: " + codelistName + " :: " + codeValue
	);
}

return result;

Call example:

var description = Library:CodeListUtilities.GetDescription(
		"http://sap.com/xi/AP/Common/GDT", 
		"CountryCode", 
		"FR"
	);
Trace.Info(description); // => "France"

As we can see, as earlier step, it is mandatory coding the transformation in ABSL as a custom reuse library function. This makes the ability to expose the function for futher use, either in our action/events ABSL scripts or in the UI as a dedicated field with a field transformation binded to reuse functions implemented in some library.

At Business Object Level

If we choose handling the description at BO level, because also need for example to expose it to webservices, we only have to modify the BODL definition and add a transient element of type text. For example:

import AP.Common.GDT as apCommonGDT;
import AP.PDI.bo as pdi;

businessobject MyCustomBO
{
	....

	[Label("Country")]
	element country: CountryCode;

	[Label("Location")]
	[Transient]
	element Location: pdi:String;

	....
}

and handle the value resolution in the AfterLoading and AfterModify BO events instead of in an UI event handler operation we explain before. Remember check mass-enable when create the scripts as part of performance best practices.

MyCustomBO-Root-Event-AfterLoading.absl
MyCustomBO-Root-Event-AfterModify.absl
foreach (var instance in this)
{
	instance.location = Library:CodeListUtilities.GetCountryDescription(instance.country);
}

Then, one more time in the floorplan we want to handle this feature:

  1. Open floorplan in UI Designer.
  2. In the DataModel, if not yet added, add a field binded to our BO codelist element:
    Name: country
    CCTS Type: code
    Binding Field: -~country
    Requires roundtrip: True
  3. In the DataModel, if not yet added, add a field binded to our BO location element:
    Name: location
    CCTS Type: text
    Type: string
    Binding FIeld -~location
    Scope dependant data elements
    • /Root/country
  4. In the Controller define a new OutPort to provide data to our mashup:
    Name: LocationAddressOutPort
    Port Type Package: the same PTP file that we provide to the Mashup PB: e.g. /XXXX_MAIN/SRC/Mashups/Mashups.PTP.uicomponent
    Port Type Reference: the same value of port type when we create that PTP. E.g.: Inport
    Parameters:
    1. ParameterName: Address
    ParameterBinding: /Root/location

    and remember bind Mashup and create a navigation to it mapping Outport fields to Mashup. It’s easy since have the same PTP: LocationAddressOutPort::Address -> Inport::Address

  5. Go back to DataModel. Edit country field added in step 2 to provide it an OnValueChange event handler. With this, when we change the value in your BYD tenant application, the mashup will be updated again inmediately.
    Name: OnCountryValueChange
    Operations:
    1. Type: SyncDataContainer
    Name: RefreshData
    Finalize: Checked
    2. Type: FireOutport
    Name: SendData
    OutPort: LocationAddressOutPort

All the magic are made by 5th step and roundtrip property defined at 2nd step.

Using Field Transformations

But as I said before, there are another way that also takes the advantage of built-in functions: using field transformations. Let’s code an implementation about them.

After creating our built-in function, follow this steps in floorplans:

  1. Open floorplan in UI Designer.
  2. In the DataModel, if not yet added, add a field binded to our BO codelist element:
    Name: country
    CCTS Type: code
    Binding Field: -~country
  3. In the DataModel, add a new field where we’ll bind the transformation:
    Name: location
    Type: string
    Work Protect Relevant False
  4. In the Controller add a new Field Transformation. At the moment we select a target field, it’s converted to a dedicated field and is setup their type and CCTS type properties to the respectively compatible with returned value by the built-in function.
    Target Field: /Root/location
    Namespace: Select your solution namespace where your built-in function is placed.
    Transformation CodeListUtilities__GetCountryCodeDescription
    Parameters: Since built-in function have parameters, we make a bind…
    countryCode BO Binding
    BO MyCustomBO
    BO Field -~country

    Here I can make a tip to you that field transformation does not handle very well structured types in both, input and output. If you read that post and comments, we should only use simple types such as string, indicator, number … as well as their simple derivatives, like codelists. In the case of identifiers (EmployeeID, UUID …) the content of the same is passed to the built-in, so we will have to define this parameter in our function as any string type.

  5. In the Controller define a new OutPort to provide data to our mashup:
    Name: LocationAddressOutPort
    Port Type Package: the same PTP file that we provide to the Mashup PB: e.g. /XXXX_MAIN/SRC/Mashups/Mashups.PTP.uicomponent
    Port Type Reference: the same value of port type when we create that PTP. E.g.: Inport
    Parameters:
    1. ParameterName: Address
    ParameterBinding: /Root/location

    and remember bind Mashup and create a navigation to it mapping Outport fields to Mashup. It’s easy since have the same PTP: LocationAddressOutPort::Address -> Inport::Address

  6. Go back to DataModel. Edit country field added in step 2 to provide it an OnValueChange event handler. With this, when we change the value in your BYD tenant application, the mashup will be updated again inmediately.
    Name: RefreshJobLocationMashup
    Operations:
    1. Type: SyncDataContainer
    Name: RefreshData
    Finalize: Checked
    2. Type: FireOutport
    Name: SendData
    OutPort: LocationAddressOutPort

All the magic are made by 4th step and the SyncDataContainer operation in 6th step.

 


End of story pals!

I hope this tech article about the ways of coding this feature in our SAP Application Studio helps you.

Let me know if this could be done in a better way or something needs to be added.

Thanks for going through.

David Ordás Pacios

IT Crafter

4 Comments
You must be Logged on to comment or reply to a post.
  • Great blog and well described!

     

    Just saying, that Google maps api works great without country description. The country code is good enough. What would be a better use case example is the State field. Since it’s a country dependent code list 😉

    • Thanks for your suggestions Andrey

      It could be that my use case a very obvious but it was I want resolve in last openSAP course. As you say, very self-explained to be extrapolated to a more specific address field like a country state codelist.

      King regards 🙂