Today I was creating some alert rules and needed to make some modifications to the alerting functionality. Something didn’t go quite as expected, so I wanted to debug the execution of an alert action.
Here is how you can do it :
- Make sure the debug mode is set to ‘when breakpoint’ for the user linked to the alert
- Put a breakpoint in the EventJobCud class, method Run
- Change the EventJobCus class, method Run so that it does not use the runAs command :
( Revert these changes when the debugging is done, but for now you need to make this change otherwise you will not be able to step into the EventAction classes for debugging)
void run()
{
EventCUD event;
container params;
RunAsPermission runAsPermission;
;
this.expandCudRecords();
while select event
group by UserId
where (event.Status == BatchStatus::Waiting) &&
(event.CompanyId == curext())
{
params = connull();
runAsPermission = new RunAsPermission(event.UserId);
runAsPermission.assert();
//BP Deviation Documented
//runas(event.UserId, classnum(EventJobCUD), staticmethodstr(EventJobCUD, runCudEventsForUser),params,
// curext(), EventJobCUD::getLanguageId(event.UserId));
EventJobCUD::runCudEventsForUser(params);
CodeAccessPermission::revertAssert();
}
this.cleanupExecuting();
}
When you want to run for example a command prompt from within the X++ language, you can use the following code :
Let’s assume that the following parameters were passed to the method :
- FileName : “C:\windows\system32\cmd.exe”
- Arguments : “”
- RunAsAdmin : true
- UseShellExecute : true
- WaitForExit : false
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
| static void runWindowsProcess( FileName _fileName
, str _arguments
, boolean _runAsAdmin = false
, boolean _useShellExecute = false
, boolean _waitForExit = false)
{
System.Diagnostics.ProcessStartInfo processStartInfo;
System.Diagnostics.Process process;
System.OperatingSystem operatingSystem;
System.Version operatingSystemVersion;
System.Exception ex;
int major;
Boolean start;
;
try
{
// Assert CLRInterop permission
new InteropPermission(InteropKind::ClrInterop).assert();
// BP deviation documented
processStartInfo = new System.Diagnostics.ProcessStartInfo(_fileName, _arguments);
// BP deviation documented
process = new System.Diagnostics.Process();
// Get an instance of the System.OperatingSystem class
operatingSystem = System.Environment::get_OSVersion();
// Get the OS version number
operatingSystemVersion = operatingSystem.get_Version();
// If admin rights asked
if(_runAsAdmin)
{
major = operatingSystemVersion.get_Major();
// Get the major part of the version number (Starting from Windows Vista)
if(major >= 6)
{
// Start using the runAs property which will prompt for administrator credentials
processStartInfo.set_Verb('runas');
}
}
// Use the system shell to execute the process
processStartInfo.set_UseShellExecute(_useShellExecute);
// Attach the starting information to the process
process.set_StartInfo(processStartInfo);
// Start the process and wait for it to complete
start = process.Start();
// Wait for the process to finish
if(_waitForExit)
{
process.WaitForExit();
}
// Revert permission
CodeAccessPermission::revertAssert();
}
catch(Exception::CLRError)
{
// BP Deviation documented
ex = ClrInterop::getLastException();
while (ex != null)
{
error(ex.ToString());
ex = ex.get_InnerException();
}
throw error(strFmt("@HEL270", _fileName));
}
} |
Owkay, have a seat and hold on to whatever you can hold on to…
As of the beginning of AX (Axapta) we have always been used to working with AOD files. But it seems that the days of the AOD fileformat are numbered !
There are a couple of reasons why the use of AOD files has become slightly obsolete :
- The MorphX environment has been decreasing in performance because of the increasing number of objects in the AOT
- Searching for text in the AOT nodes takes a huge amount of time
…
So if AOD files don’t cut it anymore, what is there to be used then? Well Dynamics Ax 6.0 will be the first version ever to have the AOT metadata stored in the SQL Server backend !!
For the users using MorphX there is no difference except for the fact that it will be much more responsive, faster, … (fe searching for a text in all the methods of all forms completes in 2 seconds ! )
The next question will probably be : How are we going to transport between different Ax environments…? Well the answer is the new fileformat : *.axModel
This format is also a binary fileformat as the AOD file but they are much smaller then AOD files (Half the size) and there will be a new tool to export/import them into a SQL Database.
The client will be configurable to point to a certain model file to use in the client session. So it will be possible to fire up an Ax client and choose a certain model file to work in. Then you can create your solution and when you’re done all the changes will be available in that .axModel file and will be easily transported to other environments.
This will be a benefit in several situations, but if you want to read the full explanation, you can find a lot of info here : http://blogs.msdn.com/mfp/
Sometimes you want a process to log infolog data into a text field instead of sending it to the infolog. For example you are posting 6 custom made journalTables and if something goes wrong you want the infologdata to be stored in a Log field on the table and you want to continue with the processing of the other journalTables.
Then you will have a catch statement where you will need to get the infolog data and store it into the field but there are some difficulties :
- First you want only the data generated by your process and not the data that might have already been in the infolog.
- Second you want to clear the errors from you code from the infolog without clearing all the rest of the infolog data.
The key is to work with the infolog.line() method to remember when to start keeping track of the lines in the infolog.
We can demonstrate this with the following job :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| static void LIB_KeSae_TestInfologRemoval(Args _args)
{
int line;
str omittedText;
;
error("error1");
error("error2");
// Now we have 2 records in the infolog
// From now on we want the following entries in the infolog to be stored
// somewhere and omitted from the infolog window
// Remember the number of entries before the custom code (in this case line = 2)
line = infolog.line();
//Customer code
error("error3");
error("error4");
error("error5");
// Here we want to retrieve statement 3, 4 and 5 en clear those from the infolog
omittedText = global::LIBGetTextFromInfoLog(line + 1, true);
// Some infolog entries after our code
error("error6");
error("error7");
} |
The following code is the code from the GetTextFromInfolog method :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| public static str LIBGetTextFromInfoLog(int _startLine = 1, boolean _clearInfo = true)
{
int currentLine;
str retValue;
;
/*
Do not use infolog.infologData to get the infolog data because that creates a copy and clears the actual infolog.
Then the infolog.line() method will return 0 afterwards and you cannot clear the selected lines anymore
*/
for(currentLine = _startLine; currentLine < = infolog.line(); currentLine++)
{
retValue += 'n' + infolog.text(currentLine);
}
// _startLine -1 because the infolog method will add 1
if(_clearInfo)
infolog.clear(_startLine -1);
return retValue;
} |
The infolog.clear() method will now take the startline into account and only clear the infolog entries from our code and leave the other entries intact as shown in the result of this job :

Result of the infolog after removing entries
Sometimes you want to disable all fields on a form’s datasource and enable some of them afterwards. Put the following piece of code in the Global class and you can call it from wherever you like.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| public static void LIBSetAllTableFields(FormDataSource _formDataSource,Boolean _value)
{
DictTable dictTable = new DictTable(_formDataSource.table());
int i;
int fieldNumber;
;
for(i = 1;i < = dictTable.fieldCnt();i++)
{
fieldNumber = dictTable.fieldCnt2Id(i);
if(_formDataSource.object(fieldNumber))
{
_formDataSource.object(fieldNumber).allowEdit(_value);
}
}
} |
It is often needed to write a custom lookup method and the SysTableLookup class can be useful to create lookups from code. However the following method uses the SysTableLookup in the background but can be called easier.
When using the SysTableLookup class, for most of the simple lookups (1 datasource table) it is alway the same. You need the following :
TableId, LookupControl, LookupFields, ReturnFields, SortFields and sometimes the use of a tmpTable.
Now following method is a generic method to user the SysTableLookup class :
public static void doLookup(TableId _tableId,
Container _lookupFields,
Container _sortFields,
FormStringControl _control,
FieldId _returnItemFieldNum,
Map _queryRanges = null,
Boolean _useTmpTable = false,
Common _tmpBuffer = null
)
{
SysTableLookup sysTableLookup = SysTableLookup::newParameters(_tableId, _control);
Query query = new Query();
QueryBuildDataSource qbds;
int i;
fieldId lookupFieldId;
;
for(i=1;i <= conlen(_lookupFields);i++)
{
lookupFieldId = conPeek(_lookupFields, i);
if(lookupFieldId == _returnItemFieldNum)
sysTableLookup.addLookupfield(lookupFieldId, true);
else
sysTableLookup.addLookupfield(lookupFieldId);
}
qbds = query.addDataSource(_tableId);
for(i=1;i <= conlen(_sortFields);i++)
{
qbds.addSortField(conPeek(_sortFields, i));
}
if(_queryRanges)
{
rangeEnumerator = _queryRanges.getEnumerator();
while (rangeEnumerator.moveNext())
{
qbds.addRange(rangeEnumerator.currentKey()).value(any2Str(rangeEnumerator.currentValue()));
}
}
if(_useTmpTable)
sysTableLookup.parmTmpBuffer(_tmpBuffer);
sysTableLookup.parmQuery(query);
sysTableLookup.performFormLookup();
}
Now when you want to create a lookup you can do it easier by doing the following :
public void lookup()
{
Container fieldNums = [FieldNum(CustTable, AccountNum), FieldNum(CustTable, Name)];
Container sortFields = [FieldNum(CustTable, AccountNum)];
FieldId returnFieldId = FieldNum(CustTable, AccountNum);
Map queryRanges = new Map(Types::Integer, Types::String);
;
queryRanges.insert(FieldNum(CustTable, AccountNum), '4000');
LIBSysTableLookup::doLookup(TableNum(CustTable), fieldNums, sortFields, this, returnFieldId, queryRanges);
}
So the only thing you need to do is specify the fields, returnfields and sortfields…
Ans let’s look at the following example : We need a lookup with a temporary table. Then we can do it like this :
Container fieldNums = [FieldNum(TmpIdRef, Name), FieldNum(TmpIdRef, HelpTxt)];
Container sortFields = [FieldNum(TmpIdRef, Name)];
FieldId returnFieldId = ConPeek(fieldNums, 1);
TmpIdRef tmpTable;
;
tmpTable = LIBDifferenceAction::BuildActionClassList();
LIBSysTableLookup::doLookup(TableNum(TmpIdRef), fieldNums, sortFields, this, returnFieldId, true, tmpTable);
It is possible to attach event listeners to objects in X++.
The next code sample shows the usage of the SysEventBroker and SysEventInfo classes to handle events :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| static void SysEventTest(Args _args)
{
SysEventBroker broker = SysEventBroker::construct();
SysEventInfo info1 = new SysEventInfo("Info-1"); // catch all;
SysEventInfo info2 = new SysEventInfo("Info-2"); // catch specific class
SysEventInfo info3 = new SysEventInfo("Info-3"); // catch specific type
;
// Setup
broker.addListener(info1, classNum(Object));
broker.addListener(info2, classNum(Runbase));
broker.addListener(info3, classNum(Object), "Fire 2"); // catch Fire 2
broker.addListener(info3, classNum(Object), "Fire 4"); // and Fire 4
broker.fireEvent(classNum(CustAutoCreate), "Fire 1", "This is my life");
broker.removeListener(info2, classNum(Runbase));
broker.fireEvent(classNum(CustAutoCreate), "Fire 2", "this is my time");
broker.fireEvent(classNum(CustAutoCreate), "Fire 3", "just show me the light");
broker.removeListener(info1);
broker.fireEvent(classNum(CustAutoCreate), "Fire 4", "and I go there");
} |