Friday, July 4, 2014

Making an existing table temporary

From a developer's perspective, temporary tables store data in the same way as normal physical tables, except that the data is automatically dropped when no longer required.
They are useful in two common situations
  1. As the datasource for a form or report, where the original data is too complex to be easily queried.
  2. As temporary storage during complicated processing, to hold the results midway through the process.
CustTable  CustTableTmp;
;
CustTableTmp.setTmp();
while select CustTable
{
    CustTableTmp.data(CustTable.data());
    CustTableTmp.doInsert();
}

Select CustTableTmp;

Temporary tables

Displaying a Message Box

Displaying a Message Box


You can display a message in a modal dialog box by using the Box class.
Some methods in the Box class display a dialog box that contains multiple buttons. These methods have a parameter for choosing which button has focus when the dialog box is first displayed. This parameter is of type DialogButtonenum. Most of these multiple-button methods return a value of type DialogButton enum. This returned value enables your program to branch based on which button the user clicked.
NoteNote
The Box application class calls the DialogBox system class. Don't call it directly—always call the Box class instead.

The following are guidelines to help you create an effective message box:
  • Choose a Box method that matches your purpose.
  • Write the information text to match the buttons in the box.
  • Choose the button best suited for having initial focus.
  • Use the returned DialogButton value to direct the branching in your code.
The following table describes the Box class methods and their associated DialogBoxType system enum values.
Box class static method nameAssociatedDialogBoxTypeenum valueDescription
info
InfoBox
The OK button is the only one that the info method displays in the dialog box.
Use this method to display general information messages rather than for warning or error messages.
infoOnce
InfoBox
The infoOnce method is similar to the info method but with the following differences:
  • Your code cannot set the text in the title bar.
  • An additional header section is displayed under the title bar, and you can set the text that displays in the header.
  • More info button is present. When clicked, it opens a Web browser to the URL string that you supply through a parameter.
The infoOnce method returns void, even though it has two buttons.
infoOnceEx
InfoBox
The infoOnceEx method is similar to the infoOnce method but with the following differences:
  • The infoOnceEx method has a parameter that can be passed in to set the text in the title bar.
  • The infoOnceEx method has a Boolean parameter for choosing whether to have the caller thread continue processing while the user reads the message in the dialog box.
okCancel
OkCancelBox
The OK and Cancel buttons are displayed in the dialog box. Call this method when the user must confirm or reject an action.
stop
StopBox
The OK button is the only one displayed in the dialog box.
Use this method to display a message when the user should stop attempting their action or when a process has stopped running.
warning
WarnBox
The OK button is the only one displayed in the dialog box. Use this dialog box for a warning message.
yesAllNoAllCancel
YesToAllNoToAllBox
The buttons displayed in this dialog box are YesYes to allNoNo to all, and Cancel.
yesNo
YesNoBox
The buttons displayed in this dialog box are Yes and No. Call this method when you need the user to make a choice.
yesNoNoAllCancel
NoToAllBox
The buttons displayed in this dialog box are YesNoNo to all, andCancel.
yesNoAxaptaForm
YesNoBox
The yesNoAxaptaForm method is similar to the yesNo method but with the following differences:
  • The yesNoAxaptaForm dialog box has a different image.
  • The yesNoAxaptaForm dialog box is wider.
yesNoCancel
YesNoCancelBox
The buttons displayed in this dialog box are YesNo, and Cancel. Call this method when the yesNo method is insufficient.
yesNoOnce
YesNoBox
The yesNoOnce method is similar to the yesNo method but with the following differences:
  • The yesNoOnce dialog box has a different image.
  • The yesNoOnce dialog box has a check box that the user can select to suppress any further occurrence of an associated message.
yesYesAllNoCancel
YesToAllBox
The YesYes to allNo, and Cancel buttons are displayed.

The following example displays a message box that contains buttons labeled YesNo, and Cancel. The No button has initial focus. When a button is clicked, a message is displayed in the Print Window to indicate which button was clicked. Run this example as a job in the Application Object Tree (AOT).
static void JobBoxDemo(Args _args)
{
    DialogButton diagBut;
    str strMessage = "The No button should have initial focus.";
    str strTitle = "Title";
    ;
    diagBut = Box::yesNoCancel(
        strMessage,
        DialogButton::No, // Initial focus is on the No button.
        strTitle);
    if (diagBut == DialogButton::No)
        {
            print "The No button was clicked.";
        }
    else
        {
            print "The button that was clicked was: ", diagBut;
        }
    pause;
}

Tuesday, June 10, 2014

WinAPI::ShellExecute() for a JAR file for Banking application

Recently came across a scenario where a banking a hash program needs to be called to encrypt a payment file before sending to the Bank. The bank had just changed the hashing program from an .exe file to a .cmd, this file inturn was a batch script file which would validate and then call a JAR file with the appropriate parameters.
Issue:- While executing the .cmd file from the Command prompt the JAR file runs and hashes the file correctly, but when we use the Dynamics AX to call the .cmd file the JAR file is not executed. The command used was WINAPI::ShellExecute(Command, Parameters);

Investigation :- After preliminary checking from both the AX and command prompt we guessed that the .cmd file was not being executed due to a failure in the calling mechanism. We tried different approaches by calling the cmd.exe and then pass the parameters to mimick the manual approach. But somehow the command prompt opened up but did not execute the JAR file. Then next approach was to add all the commands inside a batch script file which would call the JAR file, so from AX we wouldn't need to specify the details in AX but would just call the batch script file. Still the execution of JAR file from AX was not happening even though we could execute the file manually.
After a lots of Google searches on the execution environment with ample trial and error my colleague identified that the issue was due to JRE execution is the root cause. Meaning that Dynamics AX looks for the Java Runtime under the 32 bit folder(under C:\Program Files (x86)\Java\bin) instead of the 64 bit installed in Windows server 2012 machine. This could be because of the legacy run time approach followed since the older versions executing the JAR files.

Conclusion:-
Install the 32 bit Java runtime on the server.

Notes:
AX job would look something like this to call the WINAPI::ShellExecute(command); where the command = @D:\TestScript.bat

And test script  batch file will have,
CD D:\HashFile\
D:\HashFile\HashProgram.cmd parm1 parm2 parm3 parmx....
Another thing to note is to link the batch file to the folder using CD D:\HashFile\ before executing the .cmd file. To point to the correct execution folder before running the command.


  Execute exe as a process from X++  X++ run a process with user credentials

How to read in Active Directory

Dynamics Ax relies on Active Directory for user authentication. And thanx to CLR Interop, you too can use Active Directory and all it's objects and properties from within Ax. You can use AD for what it is designed for: a central storage location for application data.


But how does one get to read information from the AD? In following code snippet, I'll show you how to collect a list of all users from a specific domain, with some basic information about those users.
For this, we'll use the System.DirectoryServices namespace, an easy way of getting access to Active Directory from managed code.

In order for your code to work, don't forget to edit the networkDomain variable!

static void ReadFromAD(Args _args)
{
System.DirectoryServices.DirectorySearcher DirectorySearcher;
System.DirectoryServices.SearchScope SearchScope;
System.DirectoryServices.DirectoryEntry DirectoryEntry;
 
System.DirectoryServices.SearchResultCollection SearchResultCollection;
System.DirectoryServices.SearchResult SearchResult;
 
System.DirectoryServices.PropertyCollection PropertyCollection;
System.DirectoryServices.PropertyValueCollection PropertyValueCollection;
 
str networkDomain="yourdomainhere.com";
str prefix = 'LDAP://';
 
int totalCount;
int counter;
 
str mysamaccountname;
str myusername;
;
 
try
{
DirectoryEntry = new System.DirectoryServices.DirectoryEntry(prefix + networkDomain);
SearchScope = CLRInterop::parseClrEnum('System.DirectoryServices.SearchScope', 'Subtree');
 
DirectorySearcher = new System.DirectoryServices.DirectorySearcher(DirectoryEntry);
DirectorySearcher.set_SearchScope(searchScope);
DirectorySearcher.set_Filter(strfmt('(&(objectClass=user))'));
 
SearchResultCollection = DirectorySearcher.FindAll();
 
totalCount = SearchResultCollection.get_Count();
for (counter=0; counter < totalcount; counter++)
{
SearchResult = SearchResultCollection.get_Item(counter);
DirectoryEntry = SearchResult.GetDirectoryEntry();
 
if (DirectoryEntry)
{
PropertyCollection = DirectoryEntry.get_Properties();
 
if (PropertyCollection)
{
PropertyValueCollection = PropertyCollection.get_Item('samaccountname');
mysamaccountname=PropertyValueCollection.get_Value();
 
PropertyValueCollection = PropertyCollection.get_Item('name');
myusername=PropertyValueCollection.get_Value();
 
info(strfmt('%1 - %2',mysamaccountname,myusername));
}
}
}
 
DirectorySearcher.Dispose();
SearchResultCollection.Dispose();
}
catch (Exception::CLRError)
{
error("Error reading AD");
return;
}
 
}
 
 
 
Ax has it's own routines readily available to do the job as well.  Let's look at the class xAxaptaUserManager and xAxaptaUserDetails.

Some examples:

How to get the SID from AD for a domain user:



static void GetSID(Args _args)
{   xAxaptaUserManager xUsrMgr = new xAxaptaUserManager();
   ;
   info(xUsrMgr.getUserSid('youruserid','youraddomain'));
}

Note: Remember you can user the field networkalias from table UserInfo to do a conversion from Ax user id to the domain user id.

You can use this class for various purposes. Also for checking a password.

How to validate the system password in AD from within Ax



static void CheckPassword(Args _args)
{  
    xAxaptaUserManager  xUsrMgr = new xAxaptaUserManager();
    ;
    if(xUsrMgr.validatePassword('youruserid','youraddomain','yourpassword'))
        info('Password correct');
    else
        error('Password incorrect');
}

We could get some more information from the AD regarding the user by using class xAxaptaUserDetails.

How to get the user name from AD for a user



static void GetADUserName(Args _args)
{  
    xAxaptaUserManager  xUsrMgr = new xAxaptaUserManager();
    xAxaptaUserDetails  xUsrDet;
    ;
    xUsrDet = xUsrMgr.getDomainUser('youraddomain','youruserid');
     
    if(xUsrDet)
        info(xUsrDet.getUserName(0));
}

How to get the email address from AD for a user



static void GetADEmailAddress(Args _args)
{  
    xAxaptaUserManager  xUsrMgr = new xAxaptaUserManager();
    xAxaptaUserDetails  xUsrDet;
    ;
    xUsrDet = xUsrMgr.getDomainUser('youraddomain','youruserid');
     
    if(xUsrDet)
        info(xUsrDet.getUserMail(0));
}
 

What are the tables InventSumDateTable and InventSumDateTrans used for?


When browsing the AOT, you may come across these two tables: InventSumDateTable and InventSumDateTrans.

Using your table browser, you see that they are empty.  So what are they used for?

Both tables are used by the report Physical inventory by inventory dimension, which can be found under Inventory Management - Reports - Status - Physical inventory
This report takes you back in time, calculating the stock on hand of your items for any given date in the past. 
You might experience some performance isssues when running this report, as some calculations are needed.

The report uses both tables mentioned earlier, to store these calculations.  After generating the report, a cleanup is performed.  So if you should stumble on some left over data in these tables (the user wasn't patient enough and aborted), you can safely delete any data left in these tables

Column base report in axapta 2009

A report with a true column setup. So first column 1 is filled, top to bottom, then column 2. And then over to the next page.

So like this:

And not like this:

For this, we need 2 some lesser used report methods, gotoYmm100 and mm100Left. More about these later.

Like earlier, we use an inventory report, with InventTable as our datasource, for demonstration purposes.

Create your basic report settings (add the datasource and setup some ranges, create a report design and add some fields in the body section). Now for the extras we need:

public class ReportRun extends ObjectRun
{
int curcolumn;

int mytopofpage;
int mybottomofpage;
}


We need a variable that holds the current column position.
And 2 variables, indicating the top and bottom of the page.

For the init method of our report:

public void init()
{ ;
super();

mytopofpage=423;
mybottomofpage=1000;

curcolumn=2;
element.ChangeColumn();
}


We need a method to change columns, switching between column 1 and 2.
We call this method in the init method of the report, to make sure we start printing on each page at the same, correct top position.

void ChangeColumn()
{
if(curcolumn==1)
{
curcolumn=2;
element.gotoYmm100(mytopofpage);
InventTable_Body.leftMargin(100,Units::mm);
return;
} else if (curcolumn==2)
{
curcolumn=1;
element.newPage();
element.gotoYmm100(mytopofpage);
InventTable_Body.leftMargin(20,Units::mm);
return;
}
}

In the switching columns method, we shift from one column to the other.
In case we go to column 2, we reset our cursor print position back to the top of the page. For this, we use the gotoYmm100 method. This method allows us to set the vertical position where the next report section is printed.
Then we change the left margin of our section, in order for it to appear on the right side of the page.
When we go to column 1, we set the newPage command in order to start a new page in our report. After that, we set the print position with gotoYmm100 as well, to make sure we start on the same level for both column 1 and 2. Also the left margin is reset to the left.

One thing left, the body of our report, with the executesection.

public void executeSection()
{ ;
if(element.mm100Left() < mybottomofpage) element.ChangeColumn(); super(); }


In this part, we check where we are now with our cursor on the page. If we are near the end, we need to switch columns. We use the mm100Left method, but there is also currentYmm100.

Our result will look like this:





So item numbers counting up in column one, then column two.

Now if you look at your print preview in Ax, the output may look garbled. And if you use the built-in PDF generator, dito. But if you print the report to printer or use a third party PDF generator, all looks OK.

Source

Tuesday, January 14, 2014

Read from OutLook

static void Classes_ReadFromOutlook(Args _args)
{
SysOutlookApplication sysOutlookApplication;
SysOutlook_NameSpace sysOutlookNameSpace;
SysOutlookMapiFolder sysOutlookMapiFolder;
SysOutlook_Folders sysOutlookFolders;
SysOutlook_Items collection;
COM message;
Notes messagebody;
;

#sysOutLookComDef
sysOutlookApplication = new sysOutlookApplication();
sysOutlookNameSpace = sysOutlookApplication.getNameSpace("MAPI");

sysOutlookNameSpace.logon();
sysOutlookFolders = sysOutlookNameSpace.Folders();
sysOutlookMapiFolder = sysOutlookNameSpace.getDefaultFolder(#OlDefaultFolders_olFolderInbox);

collection = sysOutlookMapiFolder.Items();
message = collection.GetFirst();

while (message)
{
info(message.subject());

message = collection.GetNext();
}
}

Create plannedTransferOrder through X++

static void PlannedTransferOrder(Args _args)
{
    ReqPo       ReqPo;
    InventDim   InventDim;
    ;
    ReqPo.clear();
    ReqPo.initValue();
    ReqPo.ReqPlanId = "Master plan Id";
    ReqPo.RefType = ReqRefType::TransferPlannedOrder;
    ReqPo.ItemId = "ItemNumber";
    ReqPo.Qty = Qty;
    ReqPo.insertFromForm(inventDim,"10000088","10000113");
}

Refresh, Reread, Research, ExecuteQuery - How to use

1. Refresh

This method basically refreshes the data displayed in the form controls with whatever is stored in the form cache for that particular datasource record. Calling refresh() method will NOT reread the record from the database. So if changes happened to the record in another process, these will not be shown after executing refresh().

2. RefreshEx

Does a redraw of the grid rows, depending on the optional argment for specifying the number of the record to refresh (and this means the actual row number in the grid, which is less useful for AX devs). Special argument values include -1, which means that all records will be redrawn, and -2, which redraws all marked records and records with displayOptions. Default argument value is -2.
This method should be used sparingly, in cases where multiple rows from the grid are updated, resulting in changes in their displayOptions, as an example. So you should avoid using it as a replacement for refresh(), since they actually have completely different implementations in the kernel.
Also, note, that refreshEx() only redraws the grid, so the controls not in the grid might still contain outdated values. Refresh() updates everything, since this is its intention.

3. Reread

Calling reread() will query the database and re-read the current record contents into the datasource form cache. This will not display the changes on the form until a redraw of the grid contents happens (for example, when you navigate away from the row or re-open the form).
You should not use it to refresh the form data if you have through code added or removed records. For this, you would use a different method described below.

How are these 2 methods commonly used?

Usually, when you change some values in the current record through some code (for example, when the user clicks on a button), and update the database by calling update method on the table buffer, you would want to show the user the changes that happened.
In this case, you would call reread() method to update the datasource form cache with the values from the database (this will not update the screen), and then call refresh() to actually redraw the grid and show the changes to the user.

Clicking buttons with SaveRecord == Yes

Each button has a property SaveRecord, which is by default set to Yes. Whenever you click a button, the changes you have done in the current record are saved to the database. So calling reread will not restore the original record values, as some expect. If that is the user expectation, you as a developer should set the property to No.

4. Research

Calling research() will rerun the existing form query against the database, therefore updating the list with new/removed records as well as updating all existing rows. This will honor any existing filters and sorting on the form, that were set by the user.
Research(true)
The research method starting with AX 2009 accepts an optional boolean argument _retainPosition. If you call research(true), the cursor position in the grid will be preserved after the data has been refreshed. This is an extremely useful addition, which solves most of the problems with cursor positioning (findRecord method is the alternative, but this method is very slow).

5. ExecuteQuery

Calling executeQuery() will also rerun the query and update/add/delete the rows in the grid. The difference in behavior from research is described below.
ExecuteQuery should be used if you have modified the query in your code and need to refresh the form to display the data based on the updated query.

formDataSource.queryRun().query() vs formDataSource.query()

An important thing to mention here is that the form has 2 instances of the query object - one is the original datasource query (stored in formDataSource.query()), and the other is the currently used query with any user filters applied (stored in formDataSource.queryRun().query()).
When the research method is called, a new instance of the queryRun is created, using the formDataSource.queryRun().query() as the basis. Therefore, if the user has set up some filters on the displayed data, those will be preserved.
This is useful, for example, when multiple users work with a certain form, each user has his own filters set up for displaying only relevant data, and rows get inserted into the underlying table externally (for example, through AIF).
Calling executeQuery, on the other hand, will use the original query as the basis, therefore removing any user filters.
This is a distinction that everyone should understand when using research/executeQuery methods in order to prevent possible collisions with the user filters when updating the query.

List all mandatory fields in the AOT

I wrote a job today that can list all the fields that are mandatory and belongs to particular tables that have particular configuration key. I thought I should share the code here, it might be helpful.
static void findMandatoryFields(Args _args)
{
 Dictionary dictionary;
 TableId tableId;
 Dicttable dictTable;
 DictField dictField;
 int fieldCounter;
 TextBuffer textbuffer = new TextBuffer();
 str fieldpath, previousText;


 dictionary = new Dictionary();

 tableId = dictionary.tableNext(0);

 while (tableId)
{
info(int2str(tableId));

tableId = dictionary.tableNext(tableId);
dictTable = new DictTable(tableId);
if (dicttable && dictTable.configurationKeyId() == configurationKeyNum(‘Bank’))
{
 fieldpath = “”;
 for (fieldcounter = 1; fieldcounter <= dictTable.fieldCnt(); fieldCounter++)
 {
 dictField = new dictField(tableId, dictfield.fieldCnt2Id(fieldCounter));
 if (dictField && dictField.mandatory() == true)
 {
 fieldpath += dicttable.name() + “:”+ dictField.name() + “\n”;
 }
 }
 previousText = textbuffer.getText();
 textbuffer.setText(previousText + fieldpath);
 }
 }

 textbuffer.toFile(“c:\\Test.txt”);
}

Calling instance methods

static void callMethod(Args _args)
{
DictClass dictClass = new DictClass(classNum(ReleaseUpdateDB60_Cust));
Object classObj = dictClass.makeObject();
DictMethod dictMethod;
int i;
for (i=1; i < dictClass.objectMethodCnt(); i++)
{
if (Global::strStartsWith(dictClass.objectMethod(i), “update”))
{
dictClass.callObject(dictClass.objectMethod(i), classObj);
}
info(strFmt(“%1, %2, %3″, dictClass.objectMethodCnt(), dictClass.objectMethod(i), dictClass.name()));
}
}

or

Code to run all methods in a class through job

static void callMethod(Args _args)
{
DictClass dictClass = new DictClass(classNum(TestClass));
DictMethod dictMethod;
int i;
for (i=1; i < dictClass.staticMethodCnt(); i++)
{
if (Global::strStartsWith(dictClass.staticMethod(i), “create”))
{
dictClass.callStatic(dictClass.staticMethod(i));
}
info(strFmt(“%1, %2, %3″, dictClass.staticMethodCnt(), dictClass.staticMethod(i), dictClass.name()));
}
}