For something I am working on, I need a queue which contains a FIELD which can contain multiple groups as in, multiple different kind of groups, below is the definition (this is all used in a Clarion to C# project)
Inside my .inc file
ExampleParameters GROUP,TYPE
ReferenceKey CSTRING(500 + 1)
ConnectionKey CSTRING(1024 + 1)
END
Account GROUP,TYPE
Id CSTRING(38 + 1)
Name CSTRING(128 + 1)
Platform LONG
Parameters &GROUP
END
Accounts QUEUE(EsA:Account),TYPE
END
Inside the application itself:
MyAccounts QUEUE(Accounts ) END
Later on in the application I have below logic to load information from the database (where I store the accounts and parameters in two separate tables (parameters contains key/value columns)
LoadToMyAccounts ROUTINE
DATA
idx LONG
ref ANY
CODE
OPEN(BoekhoudAccounts, ReadOnly);
IF ERRORCODE() THEN STOP('Kan de Boekhoud Accounts bestand niet openen:|' & ERROR() & '|') END
OPEN(BoekhoudAccountParameters, ReadOnly);
IF ERRORCODE() THEN STOP('Kan de Boekhoud Accounts Parameters niet openen:|' & ERROR() & '|') END
IF RECORDS(BoekhoudAccounts) = 0
CLEAR(BoekhoudAccounts);
BoeAcc:Id = '';
BoeAcc:InGebruik = TRUE;
BoeAcc:Name = '<PLATFORM>';
BoeAcc:Platform = 1;
ADD(BoekhoudAccounts);
CLEAR(BoekhoudAccounts);
CLEAR(BoekhoudAccountParameters);
BoeAccPar:AccountId = '';
BoeAccPar:Key = 'ReferenceKey';
BoeAccPar:Value = 'SomeValue';
ADD(BoekhoudAccountParameters);
CLEAR(BoekhoudAccountParameters);
END
SET(BoekhoudAccounts);
IF ERRORCODE() THEN STOP('Kan de Boekhoud Accounts niet in laden:|' & ERROR() & '|') END
LOOP
NEXT(BoekhoudAccounts);
IF ERRORCODE() THEN BREAK END
CLEAR(MyAccounts);
MyAccounts.Id = BoeAcc:Id;
MyAccounts.Name = BoeAcc:Name;
MyAccounts.Platform = BoeAcc:Platform;
CASE MyAccounts.Platform
OF 1
DO LoadSpecificPlatform;
ELSE
CYCLE;
END
ADD(MyAccounts);
MyAccounts.Parameters &= NULL; ! Without I got a crash
DISPOSE(MyAccounts.Parameters);
CLEAR(MyAccounts);
END
CLOSE(BoekhoudAccounts);
CLOSE(BoekhoudAccountParameters);
MESSAGE('Loaded Accounts');
LOOP i# = 1 TO RECORDS(MyAccounts) BY 1
CLEAR(MyAccounts);
GET(MyAccounts, i#);
IF ERROR() THEN BREAK END
idx = 0;
LOOP
idx += 1;
ref &= WHAT(MyAccounts.Parameters, idx);
IF ref &= NULL THEN BREAK END
! ref is always empty
MESSAGE('On Account[' & MyAccounts.Id & ']|' & WHO(MyAccounts.Parameters, idx) & ':|' & ref & '||');
END
END
LoadSpecificPlatform ROUTINE
DATA
idx LONG
ref ANY
parameters LIKE(ExampleParameters)
CODE
CLEAR(BoekhoudAccountParameters);
BoeAccPar:AccountId = MyAccounts.Id;
idx = 0;
LOOP
idx += 1;
BoeAccPar:Key = CLIP(LEFT(WHO(parameters, idx)));
IF CLIP(BoeAccPar:Key) = ''
BREAK;
END
GET(BoekhoudAccountParameters, BoeAccPar:AccountIdKeyKey);
IF ERRORCODE() THEN CYCLE END
ref &= WHAT(parameters, idx);
IF ref &= NULL THEN BREAK END
ref = CLIP(LEFT(BoeAccPar:Value));
END
MyAccounts.Parameters &= parameters;
Thing is, where I say “ref is always empty” - the messaged value is always empty - as if the reference is not populated.
So can anyone help? Or is it just bad practice to add this to a QUEUE with &GROUP
to allow dynamic parameters?
Did you assign the Parameters &GROUP reference a pointer to the actual group?
Parameters &= NEW (My Actual Group Area with the parameters definition)
It is not bad practice to add a &GROUP structure within a QUEUE. You can even pass to procedures a generic GROUP structure pointer as a parameter to do some useful generic programming.
Like anything else you have to be careful with what you NEW and DISPOSE.
Regards
Roberto Artigas
As far as I know NEW doesn’t work with GROUP - so no I didn’t NEW it - but as you can see in the last snippet, I do assign it:
MyAccounts.Parameters &= parameters;
Greetings -
You are correct. I did not scroll down enough to see the assignment.
My apologies. At this point I am not sure that I can help.
Hopefully someone else can jump in and assist you.
Regards,
Roberto Artigas
In LoadSpecificPlatform you have
MyAccounts.Parameters &= parameters
But that’s a problem as the right hand side is a variable that is scoped to the routine, so now you have a reference to memory somewhere in the stack that has been given back
Even if that were not a problem, the parameters variable in the routine is never to set anything,
TBH I’m not sure what you were trying to write.
As it was pointed out earlier CW does not let you NEW a group for some strange reason
You can work around this by allocating the bytes a NEW STRING( Size(theGroup))
or by redeclaring the group as a ctTheGroup CLASS(TheGroup),TYPE
and then NEW the class
I wonder if you’d be happier with a nested queue of Key,Value Pairs
parameters is being set inside the loop using WHAT
through a reference - I know this works because I can show them with a message.
The thing I am writing is some code which loads data from the database to a QUEUE to keep it accessible in memory - the reason I don’t use a nested QUEUE is because I can’t Marshall that to my C# dll - hence why the QUEUE is based on a GROUP I defined, the GROUP works when set manually, but I want to keep all accounts available in-memory for ease of use/access.
Not being able to NEW
a GROUP is something I think is weird, because it would solve this easily - I’ll try your two workarounds and see if that helps.
I found how to do it here: Why can't I New() a GROUP and what workaround/alternatives are there?
But it seems, when you use a STRING then WHAT
and WHO
no longer work for some reason, any fix for that?
As I think I can’t use the CLASS workaround as it means I’ll need a QUEUE per Parameter type
This is the new code I tried for LoadSpecificPlatform
but now the MESSAGE only contains the field names and no values, I checked and seems ‘NO KEY’ is triggered, so WHO
no longer returns the name of the field - any solution for that?
LoadSpecificPlatform ROUTINE
DATA
idx LONG
ref ANY
parametersString &STRING
parameters &ExampleParameters
CODE
parametersString &= NEW STRING(SIZE(ExampleParameters));
parameters &= ADDRESS(parametersString);
parameters.ConnectionKey = '';
parameters.ReferenceKey = '';
CLEAR(BoekhoudAccountParameters);
BoeAccPar:AccountId = MyAccounts.Id;
idx = 0;
LOOP
idx += 1;
BoeAccPar:Key = CLIP(LEFT(WHO(parameters, idx)));
IF CLIP(BoeAccPar:Key) = ''
BREAK;
END
GET(BoekhoudAccountParameters, BoeAccPar:AccountIdKeyKey);
IF ERRORCODE() THEN CYCLE END
ref &= WHAT(parameters, idx);
IF ref &= NULL THEN BREAK END
ref = CLIP(LEFT(BoeAccPar:Value));
END
MESSAGE(|
'ConnectionKey: ' & CLIP(parameters.ConnectionKey) & '|' |
& 'ReferenceKey: ' & CLIP(parameters.ReferenceKey) |
);
MyAccounts.Parameters &= parameters;
In C#, a single reference can point to different types, and there are operators like is
to properly identify them.
In Clarion, you could use multiple references to typed groups and NOT &= NULL
to identify the type, as shown in TestQue1 ROUTINE
below.
However, it may be simpler to just declare the groups directly in the queue, as shown in TestQue2 ROUTINE
.
PROGRAM
MAP
END
GroupAType GROUP,TYPE
NameA STRING(10)
ValueA LONG
END
GroupBType GROUP,TYPE
NameB STRING(20)
ValueB DECIMAL(15,2)
END
TestQue1 QUEUE
Id LONG
Data &STRING
GroupA &GroupAType
GroupB &GroupBType
END
TestQue2 QUEUE
Id LONG
GroupA LIKE(GroupAType)
GroupB LIKE(GroupBType)
END
CODE
DO TestQue1
DO TestQue2
TestQue1 ROUTINE
DATA
idx LONG
str ANY
CODE
!Add GroupA
CLEAR(TestQue1)
TestQue1.Id = 1
TestQue1.Data &= NEW STRING(SIZE(GroupAType))
TestQue1.GroupA &= ADDRESS(TestQue1.Data)
TestQue1.GroupA.NameA = 'Name A'
TestQue1.GroupA.ValueA = 1234
ADD(TestQue1)
!Add GroupB
CLEAR(TestQue1)
TestQue1.Id = 2
TestQue1.Data &= NEW STRING(SIZE(GroupBType))
TestQue1.GroupB &= ADDRESS(TestQue1.Data)
TestQue1.GroupB.NameB = 'Name B'
TestQue1.GroupB.ValueB = 45.67
ADD(TestQue1)
!Show Queue
str = 'Test 1|'
LOOP idx = 1 TO RECORDS(TestQue1)
GET(TestQue1,idx)
str = str &' id:'&TestQue1.Id
IF NOT TestQue1.GroupA &= NULL
str = str &' NameA:'&TestQue1.GroupA.NameA&' ValueA:'&TestQue1.GroupA.ValueA
END
IF NOT TestQue1.GroupB &= NULL
str = str &' NameB:'&TestQue1.GroupB.NameB&' ValueB:'&TestQue1.GroupB.ValueB
END
str = str &'|'
END
MESSAGE(str)
!Free Queue
LOOP idx = 1 TO RECORDS(TestQue1)
TestQue1.GroupA &= NULL
TestQue1.GroupB &= NULL
DISPOSE(TestQue1.Data)
END
FREE(TestQue1)
TestQue2 ROUTINE
DATA
idx LONG
str ANY
CODE
!Add GroupA
CLEAR(TestQue2)
TestQue2.Id = 1
TestQue2.GroupA.NameA = 'Name A'
TestQue2.GroupA.ValueA = 1234
ADD(TestQue2)
!Add GroupB
CLEAR(TestQue2)
TestQue2.Id = 2
TestQue2.GroupB.NameB = 'Name B'
TestQue2.GroupB.ValueB = 45.67
ADD(TestQue2)
!Show Queue
str = 'Test 2|'
LOOP idx = 1 TO RECORDS(TestQue2)
GET(TestQue2,idx)
str = str &' id:'&TestQue2.Id
IF TestQue2.GroupA.NameA
str = str &' NameA:'&TestQue2.GroupA.NameA&' ValueA:'&TestQue2.GroupA.ValueA
END
IF TestQue2.GroupB.NameB
str = str &' NameB:'&TestQue2.GroupB.NameB&' ValueB:'&TestQue2.GroupB.ValueB
END
str = str &'|'
END
MESSAGE(str)
!Free Queue
FREE(TestQue2)
Sadly that won’t do as I need the FIELD to have the same name as its marshalled to a struct in C# - like I said, the group itself works - but I am looking how to create a QUEUE out of it so I can keep all active accounts in memory instead of looking them up each time - but perhaps I should just give this up and just look them up when needed as it seems this cannot be done in Clarion as one cannot NEW a GROUP (for which I cannot find the reason)
One option is to use a string field and store the groups as text, using a format like JSON or CSV. It’s trivial to parse the text into a type in C#.
The group works, it does what it needs to - it’s the QUEUE I’m not getting to work as one cannot NEW a group to set the field - using a STRING to get an address works and I can set it - but WHAT and WHO won’t work anymore on the reference group field so I would be needing to hard code the fields instead of relying on a bit of dynamic resolution… So I’ll leave the QUEUE be and just create a GROUP instance where needed till I find a solution