ScrollSort and Component Interface Lost Pointer
By Chris Malek | Wed, May 27, 2015
I have seen this issue at a few clients when trying to programmatically
“term activate” students in the
STDNT_ACTIVATION component using a
component interface. This shows a strange edge case that can happen to
the pointers in your CI APIObject variables when the underlying
component sorts the data when your code is manipulating the data in a
grid or scroll area. This is an excellent example of why I always tell
the students in my Component Interface
class to be a data entry expert
on the component you are working with before writing any code against
it. The reason is that if the Component has any quirks like this, your
code calling the CI will need to have logic to handle the quirks.
The first time I saw this, it took me a while to figure out that some FieldChange PeopleCode was triggering and the CI pointer to the row was being lost.
First lets look at the UI to understand what is happening. Using a Demo instance of Campus Solutions, we pull up a test student who already has some term activation rows. In this screen-shot, the student has two rows.
We navigate to the last row and insert a third row. This leaves use a blank row where we can insert a new term.
If you look closely after you insert the new Term value, you will see that the current row is now at row number 1 instead of 3. This is subtle but it can wreak havoc on code that you have in a component Interface trying to term activate a student. The reason is that your pointer will still be at row 3. However, you really need a pointer to row 1 in this instance. The new row could have ended up anywhere and you can’t assume it is always at row 1. In the UI, the user is taken the to new row after the sort. However, this is not manifested in the “CI World”.
The source of the issue is from some code in the following events.
The offending line of PeopleCode is:
SortScroll(2, Record.STDNT_CAREER, Record.STDNT_CAR_TERM, STDNT_CAR_TERM.INSTITUTION, "A", STDNT_CAR_TERM.STRM, "D");
This will re-sort the grid based on INSTITUTION and STRM.
It turns out when you have a component interface that is inserting rows
in the component you can actually lose the pointer reference to the data
item you are working with. So you may have a data item pointer in the
&x and after you either set the institution or term value,
that pointer may no longer point to the new row you inserted.
If you write code using a component interface you need to be aware of this “feature” and expect the pointer to be lost and you need to loop through the collection and find your item/row again. The error usually manifests itself when you try to save the CI. The error message generally will say that STRM code is a required value. The reason is that your code inserted a third row. However, when you set the institution, the scrollSort moved your pointer to a different row and not your new row. Then if you set the STRM property you are actually overriding an existing row in the component.
Lets look at some high level code for this.
This first section just sets up the CI Variables and we set an EMPLID.
Local ApiObject &oSession, &oWStdntActivation; Local ApiObject &oStdntCareerCollCollection, &oStdntCareerColl; Local ApiObject &oStdntCarTermCollCollection, &oStdntCarTermColl; &oSession = %Session; &oSession.PSMessagesMode = 1; &oStdntActivation = &oSession.GetCompIntfc(CompIntfc.STDNT_ACTIVATION); If &oStdntActivation = Null Then throw CreateException(0, 0, "GetCompIntfc failed"); End-If; &oStdntActivation.InteractiveMode = True; &oStdntActivation.GetHistoryItems = False; &oStdntActivation.EditHistoryItems = False; &oStdntActivation.GetDummyRows = False; &oStdntActivation.EMPLID = "XYZ"; If Not &oStdntActivation.Get() Then throw CreateException(0, 0, "Get failed"); End-If;
Next we loop through the student career level 1 scroll looking to see if the student already has a UGRD career. Here we have, the career value hard coded to “UGRD” but you would replace that with some variable of your liking.
If the career is not there then the student has not been matriculated yet and that is done in a different component.
Local boolean &bCINeedsToBeSaved = False; Local integer &iCareer; &oStdntCareerCollCollection = &oStdntActivation.STDNT_CAREER_COLL; Local boolean &bCareerFound = false; For &iCareer = 1 To &oStdntCareerCollCollection.Count; &oStdntCareerColl = &oStdntCareerCollCollection.Item(&iCareer); If &oStdntCareerColl.ACAD_CAREER = "UGRD" Then &bCareerFound = True; Break; End-If; End-For;
This section of code will loop through the level 2 scroll looking to see if a hard coded STRM and INSTITUTION are set already. Only if they are not found, do we later attempt to insert a row in the level two scroll/collection.
local string &strmToFind = "0530" Local string &InstitutionToFind = "PSUNV"; If &bCareerFound Then Local integer &iTERM; &oStdntCareerColl = &oStdntCareerCollCollection.Item(&iCareer); &oStdntCarTermCollCollection = &oStdntCareerColl.STDNT_CAR_TERM_COLL; Local boolean &bFoundStrm; &bFoundStrm = False; For &iTERM = 1 To &oStdntCarTermCollCollection.Count; &oStdntCarTermColl = &oStdntCarTermCollCollection.Item(&iTERM); If &oStdntCarTermColl.STRM = &strmToFind And &oStdntCarTermColl.INSTITUTION = &InstitutionToFind Then &bFoundStrm = True; Break; End-If; End-For;
If the STRM and INSTITUTION were not found, then we need to insert it.
This is where we are going to run into some issues and need some special
non-standard handling. After we set either the INSTITUTION or STRM
property on the CI, we have to assume that our pointer to the row in
&oStdntCarTermColl was lost due to the scrollScort function that
triggers on the field change event. This requires us to do a
back over the collection to find the row we are working with. You will
see a slightly different
for loop after the INSTITUTION and STRM
If &bFoundStrm = False Then &oStdntCarTermColl = Null; &oStdntCarTermColl = &oStdntCarTermCollCollection.InsertItem(1); &oStdntCarTermColl.institution = &InstitutionToFind; /* On FieldChange in line above the scroll re-sorted */ For &iTERM = 1 To &oStdntCarTermCollCollection.Count; &oStdntCarTermColl = &oStdntCarTermCollCollection.Item(&iTERM); If None(&oStdntCarTermColl.STRM) And &oStdntCarTermColl.INSTITUTION = &InstitutionToFind Then Break; End-If; End-For; &oStdntCarTermColl.strm = &recInboundCarTerm.STRM.Value; /* On FieldChange in line above the scroll re-sorted */ For &iTERM = 1 To &oStdntCarTermCollCollection.Count; &oStdntCarTermColl = &oStdntCarTermCollCollection.Item(&iTERM); If &oStdntCarTermColl.strm = &strmToFind And &oStdntCarTermColl.INSTITUTION = &InstitutionToFind Then Break; /* Break when we find the row so &oStdntCarTermColl is set properly */ End-If; End-For; If &oStdntActivation.SAVE() Then /* Save success */ Else /* Handle Save Error End-If;
There are some potential other work-around to this:
- Add some custom code to the fieldChange events to ignore the
%CompIntfcNamesystem variable is NOT blank.
- Try using
InteractiveMode = False.