Return a list of all atributes of a persistent class in abap - persistence

I got a persistent class on a custom table. Now with GET_PERSISTENT_BY_QUERY I can get a list of objects. How can I now get those Objects with all of their atributes printed to the user (e.g as ALV)?
Can I call an instance function if i click on it somehow?

Here is a dynamic solution, query_data is the result returned from the call to get_persistent_by_query( ). In the end the data will be stored in structured table <table>, you can use this to display in your ALV. Note that there is little control over the order of the field. For real use you'd have to add some error handling as well.
DATA: pers_obj_desc TYPE REF TO cl_abap_classdescr,
pers_query_data TYPE REF TO data,
field_cat TYPE lvc_t_fcat.
FIELD-SYMBOLS: <table> TYPE STANDARD TABLE.
LOOP AT query_data ASSIGNING FIELD-SYMBOL(<pers_data>).
AT FIRST.
pers_obj_desc ?= cl_abap_typedescr=>describe_by_object_ref( p_object_ref = <pers_data> ).
LOOP AT pers_obj_desc->attributes ASSIGNING FIELD-SYMBOL(<attr_desc>).
APPEND INITIAL LINE TO field_cat ASSIGNING FIELD-SYMBOL(<field_cat>).
<field_cat>-fieldname = <attr_desc>-name.
<field_cat>-intlen = <attr_desc>-length.
<field_cat>-datatype = <attr_desc>-type_kind.
ENDLOOP.
cl_alv_table_create=>create_dynamic_table(
EXPORTING
it_fieldcatalog = field_cat
IMPORTING
ep_table = pers_query_data
).
ASSIGN pers_query_data->* TO <table>.
ENDAT. " first
APPEND INITIAL LINE TO <table> ASSIGNING FIELD-SYMBOL(<line>).
LOOP AT pers_obj_desc->attributes ASSIGNING <attr_desc>.
ASSIGN COMPONENT <attr_desc>-name OF STRUCTURE <line> TO FIELD-SYMBOL(<field>).
data(getter) = 'GET_' && <attr_desc>-name.
CALL METHOD <pers_data>->(getter) RECEIVING result = <field>.
ENDLOOP.
ENDLOOP.

One (i do not think good) way is to define a instance atribute called list. Then, the generated getter can be overwritten as following, which is addresed from anywhere.
DATA: WA TYPE ZMM_SMATERIAL.
WA-ERNAM = GET_ERNAM( ).
wa-ERSDA = GET_ERSDA( ).
WA-LAEDA = GET_LAEDA( ).
WA-MATNR = GET_MATNR( ).
WA-MTART = GET_MTART( ).
.
.
.
result = WA.
" GET_LIST
endmethod.
The ... stands for each attribute I want in the List.
The CONS to this solution:
on every call there is very much logic to do
the attribute is not used

An other (I do not think good) way is to define a instance atribute called list. Then, on the initialize, the list gets prepared.
DATA: WA TYPE ZMM_SMATERIAL.
WA-ERNAM = GET_ERNAM( ).
wa-ERSDA = GET_ERSDA( ).
WA-LAEDA = GET_LAEDA( ).
WA-MATNR = GET_MATNR( ).
WA-MTART = GET_MTART( ).
.
.
.
LIST = WA.
endmethod.
The ... stands for each attribute I want in the List.
Then, on every setter, also in this list it gets changed
The CONS to this solution:
Everything is redundant,
each setter has to get changed

One more way is to define a instance atribute called list. Then, the generated getter can be overwritten as following, which is addresed from anywhere.
if me->list is initial.
DATA: WA TYPE ZMM_SMATERIAL.
WA-ERNAM = GET_ERNAM( ).
wa-ERSDA = GET_ERSDA( ).
WA-LAEDA = GET_LAEDA( ).
WA-MATNR = GET_MATNR( ).
WA-MTART = GET_MTART( ).
.
.
.
me->list = WA.
endif.
result = me->list
" GET_LIST
endmethod.
The ... stands for each attribute I want in the List.
Additionaly to that, I need to CLEAR the list on every setter.
The big con to that is that the generator overwrites everything. (Without any warning.)

Based on the answer of gert beukema I have written a helper class (with major improvements like accepting more than public simple atributes without failing).
class ZCL_PS_OBJECT_TO_STRUCTURE_HLP definition
public
create private .
public section.
class-methods REFTAB_TO_STRUCTURE_LIST
importing
!IT_REFRENCE_TABLE type OSREFTAB
returning
value(RR_OBJECT_ATTRIBUTES_TABLE) type ref to DATA .
class-methods CREATE_FIELDCAT
importing
!IR_OBJ_DESC type ref to CL_ABAP_CLASSDESCR
returning
value(RO_FIELD_CAT) type LVC_T_FCAT .
protected section.
private section.
ENDCLASS.
CLASS ZCL_PS_OBJECT_TO_STRUCTURE_HLP IMPLEMENTATION.
* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Static Public Method ZCL_PS_OBJECT_TO_STRUCTURE_HLP=>CREATE_FIELDCAT
* +-------------------------------------------------------------------------------------------------+
* | [--->] IR_OBJ_DESC TYPE REF TO CL_ABAP_CLASSDESCR
* | [<-()] RO_FIELD_CAT TYPE LVC_T_FCAT
* +--------------------------------------------------------------------------------------</SIGNATURE>
METHOD CREATE_FIELDCAT.
LOOP AT IR_obj_desc->attributes ASSIGNING FIELD-SYMBOL(<fs_attr_desc>).
APPEND INITIAL LINE TO ro_field_cat ASSIGNING FIELD-SYMBOL(<fs_field_cat>).
<fs_field_cat>-fieldname = <fs_attr_desc>-name.
<fs_field_cat>-intlen = <fs_attr_desc>-length.
<fs_field_cat>-datatype = <fs_attr_desc>-type_kind.
<fs_field_cat>-outputlen = calculate a good outputlength (double when hex)
ENDLOOP.
ENDMETHOD.
* <SIGNATURE>---------------------------------------------------------------------------------------+
* | Static Public Method ZCL_PS_OBJECT_TO_STRUCTURE_HLP=>REFTAB_TO_STRUCTURE_LIST
* +-------------------------------------------------------------------------------------------------+
* | [--->] IT_REFRENCE_TABLE TYPE OSREFTAB
* | [<-()] RR_OBJECT_ATTRIBUTES_TABLE TYPE REF TO DATA
* +--------------------------------------------------------------------------------------</SIGNATURE>
METHOD reftab_to_structure_list.
DATA: lr_obj_desc TYPE REF TO cl_abap_classdescr,
lr_query_data TYPE REF TO data,
lt_fieldcat TYPE lvc_t_fcat.
FIELD-SYMBOLS: <fs_table> TYPE STANDARD TABLE.
LOOP AT it_refrence_table ASSIGNING FIELD-SYMBOL(<fs_object>).
AT FIRST.
lr_obj_desc ?= cl_abap_typedescr=>describe_by_object_ref( p_object_ref = <fs_object> ).
lt_fieldcat = create_fieldcat( ir_obj_desc = lr_obj_desc ).
CALL METHOD cl_alv_table_create=>create_dynamic_table
EXPORTING
* i_style_table =
it_fieldcatalog = lt_fieldcat
* i_length_in_byte =
IMPORTING
ep_table = rr_object_attributes_table
* ep_table = lr_query_data
* e_style_fname =
EXCEPTIONS
generate_subpool_dir_full = 1
OTHERS = 2.
* IF sy-subrc <> 0.
* do some errorhandling here
* ENDIF.
ASSIGN rr_object_attributes_table->* TO <fs_table>.
* ASSIGN lr_query_data->* TO <fs_table>.
ENDAT. "first
APPEND INITIAL LINE TO <fs_table> ASSIGNING FIELD-SYMBOL(<fs_line>).
LOOP AT lr_obj_desc->attributes ASSIGNING FIELD-SYMBOL(<fs_attr_desc>).
ASSIGN COMPONENT <fs_attr_desc>-name OF STRUCTURE <fs_line> TO FIELD-SYMBOL(<fs_attr_value>).
DATA(getter) = 'GET_' && <fs_attr_desc>-name.
TRY .
CALL METHOD <fs_object>->(getter) RECEIVING result = <fs_attr_value>.
CATCH cx_sy_dyn_call_illegal_method. "get_method is protected or private
CLEAR <fs_attr_value>.
CATCH cx_sy_dyn_call_illegal_type.
"what to do with a list?
ENDTRY.
ENDLOOP.
ENDLOOP.
ENDMETHOD.
ENDCLASS.
Now with that helperclass I do not have to recode it always I want a list out of a Object.
In a programm the call is e.G like this:
DATA: lt_query_data TYPE osreftab,
lt_query_table TYPE REF TO data.
FIELD-SYMBOLS <fs_table>.
lt_query_data = zca_ps_user=>agent->if_os_ca_persistency~get_persistent_by_query(
i_query = cl_os_system=>get_query_manager( )->create_query( )
).
CALL METHOD zcl_ps_object_to_structure_hlp=>reftab_to_structure_list
EXPORTING
it_refrence_table = lt_query_data
RECEIVING
rr_object_attributes_table = lt_query_table.
IF lt_query_table IS INITIAL.
LEAVE.
ENDIF.
ASSIGN lt_query_table->* TO <fs_table>.
*Create a ALV
DATA: lr_alv TYPE REF TO cl_salv_table .
cl_salv_table=>factory( IMPORTING r_salv_table = lr_alv
CHANGING t_table = <fs_table> )
.
lr_alv->display( ).
Ps: I wanted to edit the answer, but it was not accepted.

Related

SAP Custom Workflow is not working (LPOR missing)

I created my first custom OO Workflow.
I created the class and added the interface IF_WORKFLOW.
I implemented the methods LPOR and FIND_BY_LPOR of interface BI_PERSISTENT.
The value of INSTID should be the Workitem ID.
I just have one activity (dialog) in my workflow. When I'm testing the workflow, an error appears.
Error-Code WL-821 - Dereferencing NULL-Reference.
I created a breakpoint in my custom FIND_BY_LPOR method. There is a CHECK for LPOR-INSTID is NOT INITIAL.
Unfortunately my LPOR-INSTID is always INITIAL. LPOR-CATID and LPOR-TYPEID are filled.
Is there something I have to customize?
Code:
CONSTRUCTOR (When is the constructor called, I can't get in while debugging?)
m_workitem_id = i_wi_id.
ms_lpor-catid = swfco_objtype_cl.
ms_lpor-typeid = mc_objtype.
ms_lpor-instid = i_wi_id.
LPOR
result = ms_lpor.
FIND_BY_LPOR
DATA: ls_key TYPE zs_wf_key,
ls_instance TYPE ty_instance.
CLEAR: ls_key, ls_instance.
CHECK lpor-instid IS NOT INITIAL. "HERE ITS EXITING, BECAUSE INSTID = INITIAL
READ TABLE mt_instances WITH KEY id = lpor-instid
INTO ls_instance.
IF sy-subrc NE 0.
TRY .
ls_key = lpor-instid.
CREATE OBJECT ls_instance-instance TYPE (lpor-typeid)
EXPORTING
is_key = ls_key.
CATCH cx_bo_error cx_root
RETURN.
ENDTRY.
ls_instance-id = lpor-instid.
APPEND ls_instance TO mt_instances.
ENDIF.
result ?= ls_instance-instance.

How to modify double click on ALV event?

Hi guys!
I found a program on the internet that runs row details when I double-click the 'CARRID' cell on ALV.
How to rewrite this code so that the details are displayed no matter what cell in the table I click, not only in 'CARRID'?
CLASS lcl_handle_dc DEFINITION.
PUBLIC SECTION.
METHODS double_click
FOR EVENT double_click OF if_salv_gui_table_display_opt
IMPORTING ev_field_name eo_row_data.
ENDCLASS.
CLASS lcl_handle_dc IMPLEMENTATION.
METHOD double_click.
DATA: ls_sflight TYPE sflight.
CHECK ev_field_name = 'CARRID'.
* read the row data
eo_row_data->get_row_data(
EXPORTING
iv_request_type = if_salv_gui_selection_ida=>cs_request_type–all_fields
IMPORTING
es_row = ls_sflight ).
* Display the row data
cl_salv_ida_show_data_row=>display( iv_text = 'Flight Row Info' is_data = ls_sflight ).
ENDMETHOD.
ENDCLASS.
START-OF-SELECTION.
DATA: lr_salv TYPE REF TO if_salv_gui_table_ida,
lr_handle TYPE REF TO lcl_handle_dc.
cl_salv_gui_table_ida=>create(
EXPORTING
iv_table_name = 'SFLIGHT'
RECEIVING
ro_alv_gui_table_ida = lr_salv ).
DATA(lr_disp) = lr_salv->display_options( ).
* Enable double click
lr_disp->enable_double_click( ).
CREATE OBJECT lr_handle.
SET HANDLER lr_handle->double_click FOR ALL INSTANCES.
* Display ALV
lr_salv->fullscreen( )->display( ).
Remove the below line of code.
CHECK ev_field_name = 'CARRID'.
The CHECK statement ensures that the logical expression that follows evaluates to abap_true before continuing executing the next statements.

How to fix FirstOrDefault returning Null in Linq

My Linq Query keeps returning the null error on FirstOrDefault
The cast to value type 'System.Int32' failed because the materialized value is null
because it can't find any records to match on the ClinicalAssetID form the ClinicalReading Table, fair enough!
But I want the fields in my details page just to appear blank if the table does not have matching entry.
But how can I handle the null issue when using the order by function ?
Current Code:
var ClinicalASSPATINCVM = (from s in db.ClinicalAssets
join cp in db.ClinicalPATs on s.ClinicalAssetID equals cp.ClinicalAssetID into AP
from subASSPAT in AP.DefaultIfEmpty()
join ci in db.ClinicalINSs on s.ClinicalAssetID equals ci.ClinicalAssetID into AI
from subASSINC in AI.DefaultIfEmpty()
join co in db.ClinicalReadings on s.ClinicalAssetID equals co.ClinicalAssetID into AR
let subASSRED = AR.OrderByDescending(subASSRED => subASSRED.MeterReadingDone).FirstOrDefault()
select new ClinicalASSPATINCVM
{
ClinicalAssetID = s.ClinicalAssetID,
AssetTypeName = s.AssetTypeName,
ProductName = s.ProductName,
ModelName = s.ModelName,
SupplierName = s.SupplierName,
ManufacturerName = s.ManufacturerName,
SerialNo = s.SerialNo,
PurchaseDate = s.PurchaseDate,
PoNo = s.PoNo,
Costing = s.Costing,
TeamName = s.TeamName,
StaffName = s.StaffName,
WarrantyEndDate = subASSPAT.WarrantyEndDate,
InspectionDate = subASSPAT.InspectionDate,
InspectionOutcomeResult = subASSPAT.InspectionOutcomeResult,
InspectionDocumnets = subASSPAT.InspectionDocumnets,
LastTypeofInspection = subASSINC.LastTypeofInspection,
NextInspectionDate = subASSINC.NextInspectionDate,
NextInspectionType = subASSINC.NextInspectionType,
MeterReadingDone = subASSRED.MeterReadingDone,
MeterReadingDue = subASSRED.MeterReadingDue,
MeterReading = subASSRED.MeterReading,
MeterUnitsUsed = subASSRED.MeterUnitsUsed,
FilterReplaced = subASSRED.FilterReplaced
}).FirstOrDefault(x => x.ClinicalAssetID == id);
Tried this but doesn't work
.DefaultIfEmpty(new ClinicalASSPATINCVM())
.FirstOrDefault()
Error was:
CS1929 'IOrderedEnumerable<ClinicalReading>' does not contain a definition for 'DefaultIfEmpty' and the best extension method overload 'Queryable.DefaultIfEmpty<ClinicalASSPATINCVM>(IQueryable<ClinicalASSPATINCVM>, ClinicalASSPATINCVM)' requires a receiver of type 'IQueryable<ClinicalASSPATINCVM>'
Feel a little closer with this but still errors
let subASSRED = AR.OrderByDescending(subASSRED => (subASSRED.MeterReadingDone != null) ? subASSRED.MeterReadingDone : String.Empty).FirstOrDefault()
Error:
CS0173 Type of conditional expression cannot be determined because there is no implicit conversion between 'System.DateTime?' and 'string'
The original error means that some of the following properties of the ClinicalASSPATINCVM class - MeterReadingDone, MeterReadingDue, MeterReading, MeterUnitsUsed, or FilterReplaced is of type int.
Remember that subASSRED here
let subASSRED = AR.OrderByDescending(subASSRED => subASSRED.MeterReadingDone).FirstOrDefault()
might be null (no corresponding record).
Now look at this part of the projection:
MeterReadingDone = subASSRED.MeterReadingDone,
MeterReadingDue = subASSRED.MeterReadingDue,
MeterReading = subASSRED.MeterReading,
MeterUnitsUsed = subASSRED.MeterUnitsUsed,
FilterReplaced = subASSRED.FilterReplaced
If that was LINQ to Objects, all these would generate NRE (Null Reference Exception) at runtime. In LINQ to Entities this is converted and executed as SQL. SQL has no issues with expression like subASSRED.SomeProperty because SQL supports NULL naturally even if SomeProperty normally does not allow NULL. So the SQL query executes normally, but now EF must materialize the result into objects, and the C# object property is not nullable, hence the error in question.
To solve it, find the int property(es) and use the following pattern inside query:
SomeIntProperty = (int?)subASSRED.SomeIntProperty ?? 0 // or other meaningful default
or change receiving object property type to int? and leave the original query as is.
Do the same for any non nullable type property, e.g. DateTime, double, decimal, Guid etc.
You're problem is because your DefaultIfEmpty is executed AsQueryable. Perform it AsEnumerable and it will work:
// create the default element only once!
static readonly ClinicalAssPatInVcm defaultElement = new ClinicalAssPatInVcm ();
var result = <my big linq query>
.Where(x => x.ClinicalAssetID == id)
.AsEnumerable()
.DefaultIfEmpty(defaultElement)
.FirstOrDefault();
This won't lead to a performance penalty!
Database management systems are extremely optimized for selecting data. One of the slower parts of a database query is the transport of the selected data to your local process. Hence it is wise to let the DBMS do most of the selecting, and only after you know that you only have the data that you really plan to use, move the data to your local process.
In your case, you need at utmost one element from your DBMS, and if there is nothing, you want to use a default object instead.
AsQueryable will move the selected data to your local process in a smart way, probably per "page" of selected data.
The page size is a good compromise: not too small, so you don't have to ask for the next page too often; not too large, so that you don't transfer much more items than you actually use.
Besides, because of the Where statement you expect at utmost one element anyway. So that a full "page" is fetched is no problem, the page will contain only one element.
After the page is fetched, DefaultIfEmpty checks if the page is empty, and if so, returns a sequence containing the defaultElement. If not, it returns the complete page.
After the DefaultIfEmpty you only take the first element, which is what you want.

Roblox- how to store large arrays in roblox datastores

i am trying to make a game where players create their own buildings and can then save them for other players to see and play on. However, roblox doesn't let me store all the data needed for the whole creation(there are several properties for each brick)
All i get is this error code:
104: Cannot store Array in DataStore
any help would be greatly appreciated!
I'm not sure if this is the best method, but it's my attempt. Below is an example of a table, you can use tables to store several values. I think you can use HttpService's JSONEncode function to convert tables into strings (which hopefully can be saved more efficiently)
JSONEncode (putting brick's data into a string, which you can save into the DataStore
local HttpService = game:GetService("HttpService")
-- this is an example of what we'll convert into a json string
local exampleBrick = {
["Size"] = Vector3.new(3,3,3),
["Position"] = Vector3.new(0,1.5,0),
["BrickColor"] = BrickColor.new("White")
["Material"] = "Concrete"
}
local brickJSON = HttpService:JSONEncode(exampleBrick)
print(brickJSON)
-- when printed, you'll get something like
-- { "Size": Vector3.new(3,3,3), "Position": Vector3.new(0,1.5,0), "BrickColor": BrickColor.new("White"), "Material": "Concrete"}
-- if you want to refer to this string in a script, surround it with two square brackets ([[) e.g. [[{"Size": Vector3.new(3,3,3)... }]]
JSONDecode (reading the string and converting it back into a brick)
local HttpService = game:GetService("HttpService")
local brickJSON = [[ {"Size": Vector3.new(3,3,3), "Position": Vector3.new(0,1.5,0), "BrickColor": BrickColor.new("White"), "Material": "Concrete"} ]]
function createBrick(tab)
local brick = Instance.new("Part")
brick.Parent = <insert parent here>
brick.Size = tab[1]
brick.Position= tab[2]
brick.BrickColor= tab[3]
brick.Material= tab[4]
end
local brickData = HttpService:JSONDecode(brickJSON)
createBrick(brickData) --this line actually spawns the brick
The function can also be wrapped in a pcall if you want to account for any possible datastore errors.
Encoding a whole model into a string
Say your player's 'building' is a model, you can use the above encode script to convert all parts inside a model into a json string to save.
local HttpService = game:GetService("HttpService")
local StuffWeWantToSave = {}
function getPartData(part)
return( {part.Size,part.Position,part.BrickColor,part.Material} )
end
local model = workspace.Building --change this to what the model is
local modelTable = model:Descendants()
for i,v in pairs(modelTable) do
if v:IsA("Part") or v:IsA("WedgePart") then
table.insert(StuffWeWantToSave, HttpService:JSONEncode(getPartData(modelTable[v])))
end
end
Decoding a string into a whole model
This will probably occur when the server is loading a player's data.
local HttpService = game:GetService("HttpService")
local SavedStuff = game:GetService("DataStoreService"):GetDataStore("blabla") --I don't know how you save your data, so you'll need to adjust this and the rest of the scripts (as long as you've saved the string somewhere in the player's DataStore)
function createBrick(tab)
local brick = Instance.new("Part")
brick.Parent = <insert parent here>
brick.Size = tab[1]
brick.Position= tab[2]
brick.BrickColor= tab[3]
brick.Material= tab[4]
end
local model = Instance.new("Model") --if you already have 'bases' for the players to load their stuff in, remove this instance.new
model.Parent = workspace
for i,v in pairs(SavedStuff) do
if v[1] ~= nil then
CreateBrick(v)
end
end
FilteringEnabled
If your game uses filteringenabled, make sure that only the server handles saving and loading data!! (you probably already knew that) If you want the player to save by clicking a gui button, make the gui button fire a RemoteFunction that sends their base's data to the server to convert it to a string.
BTW I'm not that good at scripting so I've probably made a mistake somehwere.. good luck though
Crabway's answer is correct in that the HttpService's JSONEncode and JSONDecode methods are the way to go about tackling this problem. As it says on the developer reference page for the DataStoreService, Data is ... saved as a string in data stores, regardless of its initial type. (https://developer.roblox.com/articles/Datastore-Errors.) This explains the error you received, as you cannot simply push a table to the data store; instead, you must first encode a table's data into a string using JSONEncode.
While I agree with much of Crabway's answer, I believe the function createBrick would not behave as intended. Consider the following trivial example:
httpService = game:GetService("HttpService")
t = {
hello = 1,
goodbye = 2
}
s = httpService:JSONEncode(t)
print(s)
> {"goodbye":2,"hello":1}
u = httpService:JSONDecode(s)
for k, v in pairs(u) do print(k, v) end
> hello 1
> goodbye 2
As you can see, the table returned by JSONDecode, like the original, uses strings as keys rather than numeric indices. Therefore, createBrick should be written something like this:
function createBrick(t)
local brick = Instance.new("Part")
brick.Size = t.Size
brick.Position = t.Position
brick.BrickColor = t.BrickColor
brick.Material = t.Material
-- FIXME: set any other necessary properties.
-- NOTE: try to set parent last for optimization reasons.
brick.Parent = t.Parent
return brick
end
As for encoding a model, calling GetChildren would produce a table of the model's children, which you could then loop through and encode the properties of everything within. Note that in Crabway's answer, he only accounts for Parts and WedgeParts. You should account for all parts using object:IsA("BasePart") and also check for unions with object:IsA("UnionOperation"). The following is a very basic example in which I do not store the encoded data; rather, I am just trying to show how to check the necessary cases.
function encodeModel(model)
local children = model:GetChildren()
for _, child in ipairs(children) do
if ((child:IsA("BasePart")) or (child:IsA("UnionOperation"))) then
-- FIXME: encode child
else if (child:IsA("Model")) then
-- FIXME: using recursion, loop through the sub-model's children.
end
end
return
end
For userdata, such as Vector3s or BrickColors, you will probably want to convert those to strings when you go to encode them with JSONEncode.
-- Example: part with "Brick red" BrickColor.
color = tostring(part.BrickColor)
print(string.format("%q", color))
> "Bright red"
I suggest what #Crabway said, use HttpService.
local httpService = game:GetService("HttpService")
print(httpService:JSONEncode({a = "b", b = "c"}) -- {"a":"b","b":"c"}
But if you have any UserData values such as Vector3s, CFrames, Color3s, BrickColors and Enum items, then use this library by Defaultio. It's actually pretty nice.
local library = require(workspace:WaitForChild("JSONWithUserdata"))
library:Encode({Vector3.new(0, 0, 0)})
If you want a little documentation, then look at the first comment in the script:
-- Defaultio
--[[
This module adds support for encoding userdata values to JSON strings.
It also supports lists which skip indices, such as {[1] = "a", [2] = "b", [4] = "c"}
Userdata support is implemented by replacing userdata types with a new table, with keys _T and _V:
_T = userdata type enum (index in the supportedUserdataTypes list)
_V = a value or table representing the value
Follow the examples bellow to add suppport for additional userdata types.
~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~
Usage example:
local myTable = {CFrame.new(), BrickColor.Random(), 4, "String", Enum.Material.CorrodedMetal}
local jsonModule = require(PATH_TO_MODULE)
local jsonString = jsonModule:Encode(myTable)
local decodedTable = jsonModule:Decode(jsonString)
--]]

ABAP: Event CONTEXT_MENU_SELECT of CL_SIMPLE_TREE_MODEL - can't fire it

I have an instance of CL_SIMPLE_TREE_MODEL, I managed to trigger and handle the CONTEXT_MENU_REQUEST event, and I build my context menu.
I added my functions the way I wanted.
Problem is, when I select one of the options from my context menu, nothing happens. In other words, the program flow doesn't go inside the handler for CONTEXT_MENU_SELECT.
I'm of course assuming this event is fired when I click on a function in the context menu.
I found official documentation, but only for the "default context menu" that you access with Shift+F10, that needs certain subroutines in the program in order to fire.
What I did:
I did define and implement a method that is a handler for that event.
I did set the handler for the event at the same place I set the handler for the CONTEXT_MENU_REQUEST event
I did NOT register the event with the SET_REGISTERED_EVENTS because there is NO ID defined in the class attributes for that event - such as there was for the CONTEXT_MENU_REQUEST event.
The code:
REPORT.
CLASS lcl_tree_handler DEFINITION.
PUBLIC SECTION.
METHODS:
pbo,
on_ctx_menu_request FOR EVENT node_context_menu_request OF cl_simple_tree_model
IMPORTING node_key menu sender, "TYPE TM_NODEKEY CL_CTMENU
on_ctx_menu_select FOR EVENT node_context_menu_select OF cl_simple_tree_model
IMPORTING node_key fcode. "TYPE TM_NODEKEY SY-UCOMM
DATA:
po_tree_model TYPE REF TO cl_simple_tree_model,
gt_tree TYPE TABLE OF treemsnodt,
control TYPE REF TO cl_gui_control.
ENDCLASS.
CLASS lcl_tree_handler IMPLEMENTATION.
METHOD pbo.
DATA: lt_events TYPE cntl_simple_events,
ls_event TYPE cntl_simple_event.
FIELD-SYMBOLS <gs_tree> TYPE treemsnodt.
CHECK po_tree_model IS NOT BOUND.
CREATE OBJECT po_tree_model
EXPORTING
node_selection_mode = po_tree_model->node_sel_mode_single.
APPEND INITIAL LINE TO gt_tree ASSIGNING <gs_tree>.
<gs_tree>-node_key = 'Node key 1'.
<gs_tree>-text = 'First node'.
<gs_tree>-isfolder = 'X'.
APPEND INITIAL LINE TO gt_tree ASSIGNING <gs_tree>.
<gs_tree>-node_key = 'Node key 2'.
<gs_tree>-relatkey = 'Node key 1'.
<gs_tree>-relatship = cl_tree_model=>relat_last_child.
<gs_tree>-text = 'First child'.
po_tree_model->add_nodes(
node_table = gt_tree ).
ls_event-eventid = cl_simple_tree_model=>eventid_node_context_menu_req.
ls_event-appl_event = 'X'. "tried with space too
APPEND ls_event TO lt_events.
CALL METHOD po_tree_model->set_registered_events
EXPORTING
events = lt_events.
SET HANDLER on_ctx_menu_request FOR po_tree_model.
SET HANDLER on_ctx_menu_select FOR po_tree_model.
po_tree_model->create_tree_control(
EXPORTING
parent = cl_gui_container=>screen0
IMPORTING
control = control ).
ENDMETHOD.
METHOD on_ctx_menu_request. "I initialize the context menu object here.
DATA: lt_chidren_keys TYPE treemnotab,
ls_child_key TYPE tm_nodekey,
lv_text TYPE gui_text.
CALL METHOD sender->node_get_children
EXPORTING
node_key = node_key
IMPORTING
node_key_table = lt_chidren_keys
EXCEPTIONS
OTHERS = 2.
LOOP AT lt_chidren_keys INTO ls_child_key.
lv_text = ls_child_key.
CALL METHOD menu->add_function
EXPORTING
fcode = 'ONE'
text = lv_text
ftype = 'B'.
ENDLOOP.
menu->add_separator( ).
CALL METHOD menu->add_function
EXPORTING
fcode = 'ALL'
text = 'All the work groups'
ftype = 'W'.
ENDMETHOD.
METHOD on_ctx_menu_select.
BREAK-POINT. "tried actual code here too.
ENDMETHOD.
ENDCLASS.
DATA: go_tree_handler TYPE REF TO lcl_tree_handler.
PARAMETERS dummy.
INITIALIZATION.
CREATE OBJECT go_tree_handler.
AT SELECTION-SCREEN OUTPUT.
go_tree_handler->pbo( ).
AT SELECTION-SCREEN ON EXIT-COMMAND.
go_tree_handler->control->free( ).
The tree is displayed, on right click the context menu appears.
But nothing fires when I chose a menu item. Am I missing something ?
The function types you specify (ftype = 'W' and 'B') are not supported (check the fixed values of the underlying domain CUA_FUNTYP). In that case, nothing happens.
The classic solution is to use ftype = ' ' (normal function):
LOOP AT lt_chidren_keys INTO ls_child_key.
lv_text = ls_child_key.
CALL METHOD menu->add_function
EXPORTING
fcode = 'ONE'
text = lv_text
ftype = ' '.
ENDLOOP.
menu->add_separator( ).
CALL METHOD menu->add_function
EXPORTING
fcode = 'ALL'
text = 'All the work groups'
ftype = ' '.
The possible ftype values are (source: domain CUA_FUNTYP):
' ' : Normal function
'H' : Help function (PROCESS ON HELP REQUEST)
'S' : System function (handled directly by DYNP)
'T' : Transaction call (LEAVE TO TRANSACTION)
'E' : Access modules for 'AT EXIT COMMAND' -> /E as prefix
'I' : Include menu (replaced at runtime - not supported)
'N' : 'AT EXIT COMMAND' Function, > DYNP > /N as Prefix
Remark: the code in the first version of the question was missing the registering of the second event handler SET HANDLER go_tree_handler->on_ctx_menu_select FOR po_tree_model. (now it's okay)