Wednesday, July 30, 2014

Send email within AX/X++ via .NET objects or using built in SMTP objects

How to send email within AX/X++ while using the admin defined settings within AX via  .NET objects or using built in SMTP objects. Personally I prefer the system.net method.


System.Net.Mail method


static void SendEmailDotNet(Args _args)
{
    System.Net.Mail.MailMessage mailMessage;
    System.Net.Mail.SmtpClient smtpClient;
    System.Net.Mail.MailAddress mailFrom;
    System.Net.Mail.MailAddress mailTo;
    System.Net.Mail.MailAddressCollection       addressCollection;
    System.Net.Mail.Attachment                  attachment;
    System.Net.Mail.AttachmentCollection        attachementCollection;
    str    smtpServer;

    mailFrom = new System.Net.Mail.MailAddress('from@address.com',"Sender name");
    mailTo  = new System.Net.Mail.MailAddress('to@address.com',"Receipt Name");
    smtpServer = SysEmaiLParameters::find(false).SMTPRelayServerName;// using the SMTP server ip setup in Email Parameters
    mailMessage = new System.Net.Mail.MailMessage(mailFrom,mailTo);
    mailMessage.set_IsBodyHtml(true);
    mailmessage.set_Subject('This is email subject');
    mailmessage.set_Body('<html><body>This is email bodyamc</body></html>');
    addressCollection = mailMessage.get_CC();
    addressCollection.Add("cc1@address.com");
    addressCollection.Add("cc1@address.com");
   
   attachementCollection = mailMessage.get_Attachments();
   attachment = new System.Net.Mail.Attachment("C:\\test.txt"); //file to attach
   attachment.set_Name("adam.txt"); //name to display file as in email
   attachementCollection.Add(attachment);
    smtpClient = new System.Net.Mail.SmtpClient(smtpServer);
    smtpClient.Send(mailmessage);
    mailMessage.Dispose();
    info("sent");
}


AX SMTP method

static void TestEmailJob(Args _args)
{
SysEmailParameters parameters = SysEmailParameters::find();
SMTPRelayServerName relayServer;
SMTPPortNumber portNumber;
SMTPUserName userName;
SMTPPassword password;
Str subject,body;
InteropPermission interopPermission;
SysMailer mailer;
relayServer = parameters.SMTPServerIPAddress;
portNumber = parameters.SMTPPortNumber;
userName = parameters.SMTPUserName;
password = SysEmailParameters::password();
subject = "Subject line for the email";
body = "<B>Body of the email</B>";
CodeAccessPermission::revertAssert();
interopPermission = new InteropPermission(InteropKind::ComInterop);
interopPermission.assert();
mailer = new SysMailer();
mailer.SMTPRelayServer(relayServer,portNumber,userName,password, parameters.NTLM);
mailer.fromAddress("from@address.com");
mailer.tos().appendAddress("to@address.com");
mailer.subject(subject);
mailer.htmlBody(body);
mailer.attachments().add("C:\\test.txt");
mailer.sendMail();
CodeAccessPermission::revertAssert();
info("sent");
}

Catching errors vs CLR errors via try...catch

When utilizing try catch's within CLR objects (.NET extensions) I noticed that if you do not specifiy the type of catch it is then they will always go to the same catch. So the following occurs

try
{
 //do something in X++
 throw error("exit out of code because of missing data");
}
catch
{
 //the throw error lands the code logic here which is correct
}


However if I want to catch a clr error and display it, but if I throw an AX error I want some logic to occur or just ignore everything and stop the issue is they both end up at the same CLR error location. The problem is when you run the throw error it also ends up being handled like a CLR error which it is not because nothing was defined for the catch. Which is shown below


try
{
  System.Exception errorMsg;
 //do something in X++ that references a .net object (CLR)
 throw error("exit out of code because of missing data");
}
catch(Exception::CLRError)
{
 //a clr error ends here
 //but the throw error also lands here thus displaying an error message to the user that is not true
       
 errorMsg = CLRInterop::getLastException();
 info(netExcepn.ToString());
}


So in order to handle CLR errors vs AX errors cleanly you would need to do the following


try
{
  System.Exception errorMsg;
 //do something in X++ that references a .net object (CLR)
 throw error("exit out of code because of missing data");
}
catch (Exception::Error)
{
 //ax thrown errors get handled here
 return false;
}
catch(Exception::CLRError)
{
 //a clr error ends here
       
 errorMsg = CLRInterop::getLastException();
 info(netExcepn.ToString());
}



You can also do the following and it works just the same


try
{
  System.Exception errorMsg;
 //do something in X++ that references a .net object (CLR)
 throw error("exit out of code because of missing data");
}
catch(Exception::CLRError)
{
 //a clr error ends here
       
 errorMsg = CLRInterop::getLastException();
 info(netExcepn.ToString());
}
catch
{
 //ax thrown errors get handled here
 return false;
}

Thursday, July 17, 2014

Editor Extensions / Brace Matching , Highlight Words, Outlining within code

I've been using this editor extensions plugin for morphx for a couple months now on one of the PC's I develop on. Well the other day I tried to switch back to a machine that did not have it and I got so accustom to these features it didn't feel like morphx without it. So I figured I would post a link and give some credit where credit is due as I'm not sure why Microsoft didn't include these simple features within MorphX


Microsoft Dynamics AX 2012 X++ Editor Extensions
http://ax2012editorext.codeplex.com/

Brace Matching Extension

ax-ext-bracematching.png

Highlight Words Extension

ax-ext-highlightword.png

Outlining Extension

ax-ext-outlining-v2.png

Monday, July 14, 2014

Multi-Table Lookups (SysMultiTableLookup)

When creating an override lookup method that needs to return data from multiple tables you will need to be sure to use the SysMultiTableLookup class instead of SysTableLookup class. If you do not then whenever you run the fieldnum() function it will try to use the field id on the initial table instead of the one you are referring to. Below is an example on how to run a lookup to get the UOM symbols and descriptions for a special unit class uom.

 Query                   query;
    QueryBuildDataSource    queryBuildDataSourceTable;
    QueryBuildDataSource    queryBuildDataSourceTableDescriptions;
    QueryBuildRange         queryBuildRange;
    //you need to define the multitable, but initalize it AFTER you have defined the query
    //SysTableLookup          sysTableLookup  = SysTableLookup::newParameters(tableNum(UnitOfMeasure), ctrl);
    SysMultiTableLookup sysTableLookup;
    query = new Query();

    queryBuildDataSourceTable = query.addDataSource(tableNum(UnitOfMeasure));
    //join the translation table so we can get a description of the UOM
    queryBuildDataSourceTableDescriptions = queryBuildDataSourceTable.addDataSource(tableNum(UnitOfMeasureTranslation));
    queryBuildDataSourceTableDescriptions.joinMode(JoinMode::InnerJoin);
    queryBuildDataSourceTableDescriptions.relations(false);
    queryBuildDataSourceTableDescriptions.addLink(fieldNum(UnitOfMeasure,RecId),fieldNum(UnitOfMeasureTranslation,UnitOfMeasure));

    //filter by the unit class being passed
    queryBuildRange = queryBuildDataSourceTable.addRange(fieldNum(UnitOfMeasure, UnitOfMeasureClass));
    queryBuildRange.value(queryValue(unitClass));
    //define multiple table lookup query
    sysTableLookup  = SysMultiTableLookup::newParameters(ctrl, query);
    //add which fields will be displayed to the user (symbol + desc.)
    sysTableLookup.addLookupfield(fieldNum(UnitOfMeasure, Symbol), true);
    sysTableLookup.addLookupfield(fieldNum(UnitOfMeasureTranslation, Description), 2);
    //do not use for multi table
    //sysTableLookup.parmQuery(query);
    sysTableLookup.performFormLookup();

Thursday, July 3, 2014

Validate single & multiple email addresses using regular expression

One of the things I've noticed is within AX they don't do much validation of email addresses so I can enter test@123 and it would recognize it as a valid email. So if you do any sort of custom notifications the code samples below will show you have to make sure you don't try to send anything to an invalid email. You can also use the following website to check any regular expression http://rubular.com/ syntax

Check single email address

public boolean isValidEmail(str emailAddress)
{
    boolean isValidInput;
    System.Text.RegularExpressions.Match myMatch;
    str validEmailPattern = @"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$"; //pattern check for a valid email address
    try
    {
        //call function to see if the input matches the reg ex pattern
        myMatch = System.Text.RegularExpressions.Regex::Match(emailAddress, validEmailPattern);
        //check to see what the status of matching the pattern was
        isValidInput = myMatch.get_Success();
        //return to the call if the value follows the email pattern
        return isValidInput;
    }
    catch
    {
        //there was an error parsing this email so return false
        return false;
    }
}



How to check multiple emails:
(pass in a container of email addresses and return the invalid email addresses. If the container being returned has a conlen() of 0 then all of the emails are valid. You could also easily change this so it just returns a boolean if you like.


public static container checkEmailValidity(container emailAddresses)
{
    str currentEmail;
    container  invalidEmailAddresses;
    Counter emailCounter = 1;
    //check all of the email addresses against the email regular expression
    while (emailCounter <= conLen(emailAddresses))
    {
        //read the current email address from the container
        currentEmail = conPeek(emailAddresses, emailCounter);
        //check to see if the email is invalid and if it is then add it to the list
        if(!this.isValidEmail(currentEmail) && currentEmail !="")
        {
           invalidEmailAddresses += currentEmail;
        }
        ++emailCounter;
    }
    return invalidEmailAddresses;
}


Friday, June 20, 2014

Save Sales Confirmation Report To PDF w/o any alerts via X++/Code

Update: the controller seems to operate better when using ssrsController.runReport() vs ssrsController.startOperation(). It seems as though if we use runReport then the code afterwards will wait to execute  while startOperation doesn't seem to care if the file has been created to move on.

After researching how to run an SSRS report and save it to a PDF via code I found that a lot of them were incomplete and did not always work or required some odd parms or would display the report to the user even though you told it to create a file. Once I was able to get that working default AX will always alert the user of where the file was created. This doesn't make much since if you are executing this through x++ as you will usually need to do something with the file.

The following example will show you how to run a report for the latest sales confirmation and save it to a file without alerting the user.

static void SavePDFJob(Args _args)
{
    SrsReportRunController          ssrsController = new SrsReportRunController();
    SalesConfirmContract salesConfirmContract = new SalesConfirmContract();
    SRSPrintDestinationSettings     printerSettings;
    custConfirmJour     custInvoiceJour;
   
    //select the latest record based on create date
    select firstOnly custInvoiceJour
    order by custInvoiceJour.createdDateTime DESC
    where custInvoiceJour.salesid == '1000768';

    //tell the controller the report to run (filename, design name)
    ssrsController.parmReportName(ssrsReportStr(SalesConfirm, Report));
    //define how we want to execute the report (right now or batch style)
    ssrsController.parmExecutionMode(SysOperationExecutionMode::Synchronous);
    //hide the report dialog
    ssrsController.parmShowDialog(false);

    //we need to populate the required parms for the current sales order controller
    salesConfirmContract.parmRecordId(custInvoiceJour.RecId);
   
    //link the contract to the controller so we know how to run the dp
    ssrsController.parmReportContract().parmRdpContract(salesConfirmContract);

    //link the printer settings to the controller
    printerSettings = ssrsController.parmReportContract().parmPrintSettings();
    //print to pdf and always overwrite if the file exists
    printerSettings.printMediumType(SRSPrintMediumType::File);
    printerSettings.fileFormat(SRSReportFileFormat::PDF);
    printerSettings.overwriteFile(true);
    printerSettings.fileName(@"\\network\location\test.pdf");

    //run & save the report
    //ssrsController.startOperation();
    ssrsController.runReport() //refer to update at top of page
}




Optional:


The above job will create the file however you will notice you will get an info() message that says "The report has been successfully saved as: \\network\location\test.pdf"

In order to not show this to the user you will need to modify \Classes\SrsReportRunPrinter\toFile()
toward the bottom within the for loop you will see

filename = files.value(i);
info(strFmt("@SYS134644", filename));

You will need to comment out the info so the for loop will look like the following
for (i = 1 ; i <= files.lastIndex() ; i++)
        {
            // Display success in the infolog for the file(s) created
            filename = files.value(i);
            //commenting this out so we can call this function thru code without showing the user a msg
            //info(strFmt("@SYS134644", filename));
         
          // if print mgmt, then we need to copy over the file to the original file.
            // This will ensure a file specified in print mgmt settings is always there on disk. This was the shipped behavior and might keep external systems happy.
            if(printMgmtExecutionInfo)
            {
                // for reports saved in image format, each page is a image and the files are numbered. E.g. foo.gif, foo1.gif, foo2.gif
                // so we will preserve that during copying to the original location.
                fileNameToCopy = (i == 1) ? printMgmtExecutionInfo.parmOriginalDestinationFileName() : printMgmtExecutionInfo.parmOriginalDestinationFileName() + int2str(i-1);
                System.IO.File::Copy(filename, fileNameToCopy, true);
                info(strFmt("@SYS134644", filenameToCopy));
            }
        }


Wednesday, June 18, 2014

Accessing a customers contact list through x++

So I working on something where I need to access a customer contacts list through x++ however I found out that the linkage between CustTable and LogisticsElectronicAddress is far and few between so I figured I would post how I was able to regenerate the contact list that is displayed on the CustTable form

The key is to go from CustTable -> DirPartyTable -> DirPartyLocation. Once you have this you need a combination of DirPartyTable and DirPartyLocation to go to LogisticsElectronicAddress

 
 static void findEmailAckTest(Args _args)
{
    CustTable custTable;
    DirPartyTable dirParty;
    LogisticsElectronicAddress electronicAddress;
    DirPartyLocation dirPartyLoc;
   
    //find the customer
    custTable = CustTable::find("21356");
    //find the party for the customer
    dirParty = DirPartyTable::findRec(custTable.Party);
   
    //find all of the contacts for the current customer
    while SELECT  * FROM electronicAddress
    EXISTS JOIN * FROM dirPartyLoc
    WHERE electronicAddress.Location == dirPartyLoc.Location && dirParty.RecId==dirPartyLoc.Party
    {
        info(electronicAddress.Locator);
    }
    
}

Wednesday, May 28, 2014

DMF – Stack Trace error @ \Tables\DMFEntity\generateXML_WPF


It appears that whenever you go to Company/Data import export framework/Setup/Target Entities and then click on an entity then “Modify target mapping” you may get a stack trace error that is linked to \Tables\DMFEntity\generateXML_WPF and it will not allow you to open up this window for the specific entity but it will for others. To fix the error you should first try the following:

 

1.       Open \Tables\DMFEntity\generateXML_WPF and the issue is between line 201-208.

a.      What happens is the SysDictField object is called but if the table no longer contains the field in the mapping then it generates a “null” instance of SysDictField which is called on line 204 which causes an error to be thrown.

2.       We need to modify the code base for DMF so it can handle null objects and let us know where the issue is

3.       The following from line 196-209 should be changed:

Orignal:

// Generate Target Fields

    while select Entity, XMLField, TargetTable, TargetField from targetFields
        where targetFields.Entity   == _entity.EntityName  &&
              targetFields.XMLField != #blank
    {
        dictField = new SysDictField(tableName2id(targetFields.TargetTable),fieldName2id(tableName2id(targetFields.TargetTable),targetFields.TargetField));
        xmlElement = xmlDocument.createElement(#column); //Column
        xmlElement.setAttribute(#name, targetFields.XMLField); //Name
        xmlElement.setAttribute(#type, enum2str(dictField.baseType())); //Type
        xmlElement.setAttribute(#isActive,  enum2str(boolean::true));
        xmlElement = childNode.appendChild(xmlElement);
        xmlElement.setAttribute(#Label, dictField.label());
        xmlElement.setAttribute(#Help, dictField.help());
    }

 

New Code:

  // Generate Target Fields
    while select Entity, XMLField, TargetTable, TargetField from targetFields
        where targetFields.Entity   == _entity.EntityName  &&
              targetFields.XMLField != #blank
    {
        dictField = new SysDictField(tableName2id(targetFields.TargetTable),fieldName2id(tableName2id(targetFields.TargetTable),targetFields.TargetField));
       // check to see if the dictField is null, if it isn’t then we can handle the field but if it is then we need to alert the user of the table and field that should be removed from the mapping
if(dictField != null)
        {
            xmlElement = xmlDocument.createElement(#column); //Column
            xmlElement.setAttribute(#name, targetFields.XMLField); //Name
            xmlElement.setAttribute(#type, enum2str(dictField.baseType())); //Type
            xmlElement.setAttribute(#isActive,  enum2str(boolean::true));
            xmlElement = childNode.appendChild(xmlElement);
            xmlElement.setAttribute(#Label, dictField.label());
            xmlElement.setAttribute(#Help, dictField.help());
        }
        else
        {
                // alert the user of the bad mapping
info("error-table: " + targetFields.TargetTable + "  field: " + targetFields.TargetField);
        }
    }

 

 

4.       Once you add this bit of code you should compile the table and rerun the target mapping. This time the window should open but will give you am info box of the bad field.

5.       Once you have this field we now need to remove the field from the mapping. However sometimes it does not exists within the visual mapping so we need manually remove it via the following

 

static void FixEmerysDMFProductIssues(Args _args)

{

    DMFTargetXML dmftargetxml;

    delete_from dmftargetxml

    where dmftargetxml.targettable == '<table name from step 3 in info box>' && dmftargetxml.targetfield == '<field name from step 3 in info box>';

}

 

6.       Whenever you open the target mapping windows for the specific entity you should no longer get the error or a msg with a table/field info that we defined in step 3.

Wednesday, May 21, 2014

server 2012 not being able to load custom dll/reference .dll compile issues on AOS

So I ran into this issues a while back and I figured I would share how I fixed it.


So we are using a custom WYSIWYG WinForm editor (C#/.NET 4.0/Targeted for x86/x64) control inside of AX. However I noticed that whenever I went to do a compile locally  or tried to even open the form on the AOS it would always get an error and tell me that it couldn't find the reference. Even though it was in all of the .dll folders (manually bin folder copy and dynamic auto deploy temp folder)

What I figured out was all of the local and terminal server clients had Visual Studio installed on it. So I tried installing Visual Studio 2010 on the AOS server itself. Once that it done then you can compile your objects that reference outside .dll's and open the form successfully

I'm not too sure why this fixes the issues, my guess is its one of the many runtime packages that gets installed with Visual Studio fixes the issue.

Would anyone else know why this fixes it?

But if you want a quick fix just install Visual Studio and all of the compile reference issues should go away.

Sub Reports within AX SSRS Reports

​So while trying to add a subreport (no parms) to an existing report I discovered some goofy things that need to be followed in order for a sub report to work within AX.



1. Put both reports within the same project (model project within Visual studio)

2. Right Click on the sub report and choose properties. You have to manually type in the report name you want to use instead of being given a dropdown like normal SSRS

3. Include the design name (ReportName.Report, ReportName.PrecisionDesign1, etc...) within the report name mentioned in step 2.

4. Deploy the report to the same folder on the SSRS server (you may need to "move" your report into StaticReports> language> folder)



5. Previewing of subreports does not work within visual studio for AX sub reports. Once you follow steps 1-4 you should be able to add the model to the AOS. Then deploy your modified report and the subreport should show up correctly whenever you call the report from AX.





I sure do hope microsoft has some big enhancements for SSRS in 2015.