Category Archives: X++

SystemSequences Update


If you ever get a error where the RecId being inserted into the table already exists then the SystemSequences table is to be blamed for this

In some of my previous blog posts, i update the system sequence table’s NextVal record, which determines the RecId to be given. However, this is tricky.

  1. Update in SQL
    • Stop the AOS
    • Update the NextVal recod in the SystemSequences Table
    • Start the AOS
  2. Write a job
static void SetNextRecId(Args _args)
{
    SystemSequences seq;
    ttsBegin;
    select firstonly forupdate crosscompany seq
        where seq.tabId == 123456; // use the table id here or tablenum()
    seq.skipTTSCheck(true);
    seq.skipDatabaseLog(true);
    seq.selectForUpdate(true);
    seq.nextVal = 5637123456 + 1; // enter the last recId for the table
    seq.update();
    ttsCommit;
}

You may need to run this job and restart the AX client only if the just the above doesn’t work

static void sab_recIdSequenceFix(Args _args)
{
    SystemSequence systemSequence = new systemSequence();
    Tableid tableId = 123456; // use the table id or tablenum() here

    systemSequence.suspendRecIds(tableId);
    systemSequence.suspendTransIds(tableId);
    systemSequence.flushValues(tableId);
    systemSequence.removeRecIdSuspension(tableId);
    systemSequence.removeTransIdSuspension(tableId);
}
Advertisements

X++ and Ax7 Whats Changed


A lot of changes have taken place in the X++ language and to align it close to the C# model which is where we want to be. There is no longer any p-code compilation, everything is run from the CIL.

This my compilation of what has changed and some examples for them as well.

Editing Experience: Visual studio interface only. Finally a more structured way of viewing code.

Declare anywhere

for(int i = 0; i< 10; i++)
{
  // variable i can only be referenced here
}
// Try to retrieve i here and you will receive a compile error

The var keyword: ⭐

</pre>
<pre>var counter = 0;
var custTable = salesTable.custTable();

Static constructor and fields: Static member variables Static constructors: TypeNew

class AClass
{
    static string staticString;
    static void TypeNew()
    {
        AClass::staticString = "Static World!";
    }
}

Macros: As i suspected, try avoiding these guys. Will be deprecated in later releases (my view) const (constant) and readonly:

class Square
{
    public const int Sides = 4; // this value cannot be changed
    readonly real _length = 10; // this is a default

    void new(real length)
    {
        this._length = length; // readonly can be changed in constructors only
    }
}
Field Access: :star:
Field access was only protected in Ax (no questions asked, and nothing else you can do about it)
Now: if you havent specified the access they are still protected, but you can specify public, or private
class MyVariables
{
    int myInt;
    protected int myInt2;
    public string Name;
    private real _qty;
}
class MyVariables2 : MyVariables
{
    void WhatCanIDo()
    {
        myInt++;
        myInt2 = myInt + 10;
        Name = "My New World!";
        _qty = 100; // Compilation error
    }
}

class AccessVariables
{
    void WhatCanIDo()
    {
        var myVariables = new MyVariables();
        myVariables.myInt = 10; // Error
        myVariables.myInt2 = 100; // Error
        myVariables.Name = "Hello";
        myVariables._qty = 100; // Error
    }
}

Access variables by reference
Lets the above example

class MyVariables
{
    int myInt;
    public string Name;

    public void AccessVarisables()
    {
        this.myInt = 5;// This references to itself,and works with intellisense 🙂
        this.Name = "Hello World!";
    }
}
class Access
{
    public AccessVariables()
    {
        var myVars = new MyVariables();
        myVars.Name = "Hello New world!"; // Name is accessed via the class object,no more Parm methods required;
    }
}

try / catch / Finally: ⭐

try
{

}
catch(Exception::Error)
{
    // you can add multiple catch types as normal
}
finally
{
   // this code always gets called. So you can do things like close file handles
}

using statement: ⭐
Using is here, this is very neat. It destroys the object and the object only exists within the context of the using. Object in the using should implement IDisposable, so all / most of the Streams are an example or webRequests.

using(var sw = new System.IO.StreamWriter("file.txt"))
{
     sw.writeLine("Hello World!");
}

Implement .Net Classes:
Classes in X++ can implement the C# classes 🙂 , so your X++ class can implement IDisposable

Extension Methods:

There are very similar to C# extensions, except the convention is to end the class with _Extension and not Extension

static class HelloWorld_Extensions // Convention is to have _Extension
{
// This adds a method to the salesTable called SalesString returning a string
// salesTable.SalesString()
public static str SalesString(SalesTable salesTable)
{
return salesTable.SalesId + "-" + salesTable.CustName();
}

// you can add more extensions here targetting different types
}

Declarative Eventing (Methods and objects):

Ax 2012 gave us the ability to add events to methods. This is still the same, i.e. events still exist, exept they are called declaratively (i.e. in code and no clicks).

Although, the way to access arguments is still not to my liking and even though it seperates code out a lot, any errors are at runtime only, which can be very “hard” to manage

Casting is standard now:

// Planet class is implemented by class Earth.
var Earth = new Planet(); // this is incorrect, used to work before

AX 2012 – Add elements to version control


Ive written a few jobs to scan the AOT and add it to version control.
(manged to misplace the job a few times now)

I stumbled across the following post regarding the same: Objects not in Version Control (AX 2012)

However, i changed some code in there to make sure that the job types were valid (like DEL_ names didnt get added to version control)

Following is the change i made to the code mentioned in the link above:

if(vcsSys.allowCreate(controlable))
{
    //sysTreeNode is of type SysTreeNode
    sysTreeNode = sysTreeNode::construct();
    sysTreeNode.parmTreeNode(pNode);
    if(sysTreeNode.canCreate() == true)
    {
        //do something if it isn't in version control
        info(strfmt("%1 %2",pNode.treeNodePath(), modelName));
        vcsSys.commandAdd(controlable); //SHS add to version control
    }
}

This job still requires refinement because it still adds Private projects which i want to avoid

Dynamics Ax Logging – Log4net


Earlier I had posted about how to achieve File based logging in Ax, however after working with log4net on another project with .net I decided to implement debugging with Ax2012. This is because popping messages in the info log isint a very refined, besides at a customers site, after turning a few configuration keys on, the logging messages can be traced without writing extra code.

Log4Net: This is a very versatile library, if you look at the configuration files or Google around this, you will notice that it supports a wide variety of logging facilities and filters that can be applied. The overhead of using this library isint much.

The advantage of using is that the logging messages can be left inside the code (and you can even mark their category, and it can be configured in a certain way so that the debugging mechanism can be switched turned on or off for that category.

Implementing log4net in AX2012:

You could follow one of these posts and run an independent c# console project and test the log4net framework like in this blog. I wont be going through that part. What i will be showing is how to implement a library once created and added to AX2012, as AX2012 has its own configuration file which will need to be amended.

The C# project:

Create a C# project, add it to the AOT and add the log4net.dll to the c# project. Note that if you use Nuget there are issues adding it into Ax as the dll needs to be referenced with Ax too. So NuGet will not exactly help.

The code is something like this for me:

 

namespace SHSAx.Logger
{
    public enum LogType
    {
        Debug,
        Info,
        Warn,
        Error,
        Fatal
    }

    public class AxLogging
    {
        private static ICollection configure = XmlConfigurator.Configure();
        private static readonly ILog log = LogManager.GetLogger("DynamicsAx_Logging");

        private readonly ILog localILog;
        //private ILog log = null;

        //Reloads config file so that any change made to the log4net section doesnt require an AOS restart
        private static void ReloadConfiguration()
        {
            ConfigurationManager.RefreshSection("log4net");
            configure = XmlConfigurator.Configure();
        }

        public AxLogging()
        {
            ReloadConfiguration();
            localILog = log;
            //log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);
        }

        public AxLogging(string logName)
        {
            ReloadConfiguration();
            localILog = LogManager.GetLogger(logName);
        }

        //Logging for custom log Name
        public void WriteO(LogType logType, string message)
        {
            this.WriteO(logType, message, null);
        }
        public void WriteO(LogType logType, string message, Exception ex)
        {
            switch (logType)
            {
                case LogType.Debug:
                    localILog.Debug(message, ex);
                    break;
                case LogType.Info:
                    localILog.Info(message, ex);
                    break;
                case LogType.Warn:
                    localILog.Warn(message, ex);
                    break;
                case LogType.Error:
                    localILog.Error(message, ex);
                    break;
                case LogType.Fatal:
                    localILog.Fatal(message, ex);
                    break;
                default:
                    break;
            }
        }

        //Static logging - using base log name i.e. DynamicsAx_Logging
        public static void Write(LogType logType, string message)
        {
            Write(logType, message, null);
        }
        public static void Write(LogType logType, string message, Exception ex)
        {
            switch (logType)
            {
                case LogType.Debug:
                    log.Debug(message, ex);
                    break;
                case LogType.Info:
                    log.Info(message, ex);
                    break;
                case LogType.Warn:
                    log.Warn(message, ex);
                    break;
                case LogType.Error:
                    log.Error(message, ex);
                    break;
                case LogType.Fatal:
                    log.Fatal(message, ex);
                    break;
                default:
                    break;
            }
        }
    }
}

Create a config file called log4net.config (and this can be duplicated across to the AOS Server – More later)
Add the log4Net assembly to AX references. This is because the log4net assembly doesn’t get uploaded to the server directory, this double up is required as the log4net dll may not be stored in the csharp project in the AOT. Once this is done, then we are ready to call it from X++ or from other C# projects.

<?xml version="1.0" encoding="utf-8" ?>
  <log4net>
	<appender name="LogFileAppender" type="log4net.Appender.RollingFileAppender">
		<param name="File" value="C:\Temp\AxLogs\MyFirstLoggerAx000001.log"/>
		<lockingModel type="log4net.Appender.FileAppender+MinimalLock" />
		<appendToFile value="true" />
		<rollingStyle value="Size" />
		<maxSizeRollBackups value="2" />
		<maximumFileSize value="1MB" />
		<staticLogFileName value="true" />
		<layout type="log4net.Layout.PatternLayout">
			<param name="ConversionPattern" value="%d [%t] %-5p %c %m%n"/>
		</layout>
	</appender>
    <appender name="LogFileAppender2" type="log4net.Appender.RollingFileAppender">
		<param name="File" value="C:\Temp\AxLogs\MyFirstLoggerAx000002.log"/>
		<lockingModel type="log4net.Appender.FileAppender+MinimalLock" />
		<appendToFile value="true" />
		<rollingStyle value="Size" />
		<maxSizeRollBackups value="2" />
		<maximumFileSize value="1MB" />
		<staticLogFileName value="true" />
		<layout type="log4net.Layout.PatternLayout">
			<param name="ConversionPattern" value="%d [%t] %-5p %c %m%n"/>
		</layout>
	</appender>
	<appender name="LogFileAppenderPickList" type="log4net.Appender.RollingFileAppender">
		<param name="File" value="C:\Temp\AxLogs\MyFirstLoggerAx__PickList.log"/>
		<lockingModel type="log4net.Appender.FileAppender+MinimalLock" />
		<appendToFile value="true" />
		<rollingStyle value="Size" />
		<maxSizeRollBackups value="2" />
		<maximumFileSize value="1MB" />
		<staticLogFileName value="true" />
		<layout type="log4net.Layout.PatternLayout">
			<param name="ConversionPattern" value="%d [%t] %-5p %c %m%n"/>
		</layout>
	</appender>
	<appender name="TraceAppender" type="log4net.Appender.TraceAppender">
		<layout type="log4net.Layout.PatternLayout">
			<conversionPattern value="%date [%thread] %-5level %logger [%property{NDC}] - %message%newline" />
		</layout>
	</appender>

	<root>
		<level value="ALL" />
		<appender-ref ref="TraceAppender" />
		<appender-ref ref="LogFileAppender" />
	</root>

	<logger name="ShashiTest">
		<level value="NONE" />
		<appender-ref ref="LogFileAppender2" />
	</logger>
	<logger name="ThisIsMyLog">
	    <level value="Fatal" />
		<appender-ref ref="LogFileAppender2" />
	</logger>
	<logger name="Picking_list_posting">
	    <level value="WARN" />
		<appender-ref ref="LogFileAppenderPickList" />
	</logger>
</log4net>

The app.config doesnt need to be appended here, but adding it here would document it upto some level on where / what to edit in the Main config file

<?xml version="1.0"?>
<configuration>
	<configSections>
		<section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net"/> <!--This is required for log4net to work-->
	</configSections>
	<log4net configsource="log4net.config"/> <!--picks up the config from log4net.config we created earlier-->
	<startup>
		<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0"/>
	</startup>
</configuration>

Now when you were creating the c# project you would have realized that you added parts to the configsections tag in the app.config and also added the log4net tags. These settings need to be transferred into the Ax config file. So in the applications server directory you will see a file ax32serv.exe.config , this is the file where you need to add the values too (: this is the same file where AIF services are registered)
Make the changes to the ax32serv.exe.config file so that we can start programming.

Log4Net’ing from X++
I create a Class called Class1 (a very original name)

public static server void writeLog()
{
    //This uses the default log name - which is hardcoded in my project
    System.Exception exc;
    SHSAx.Logger.AxLogging logging;

    SHSAx.Logger.AxLogging::Write(SHSAx.Logger.LogType::Info, "this is an info");
    SHSAx.Logger.AxLogging::Write(SHSAx.Logger.LogType::Debug, "this is a debug");
    SHSAx.Logger.AxLogging::Write(SHSAx.Logger.LogType::Error, "this is an Error");
    SHSAx.Logger.AxLogging::Write(SHSAx.Logger.LogType::Fatal, "this is an Fatal issue");
    SHSAx.Logger.AxLogging::Write(SHSAx.Logger.LogType::Warn, "this is a Warning");
}
public static server void WriteLog1()
{
    //This uses the the lognames specified dynamically (See constructor)
    SHSAx.Logger.AxLogging logging;

    logging = new SHSAx.Logger.AxLogging("Picking_list_posting");
    logging.WriteO(SHSAx.Logger.LogType::Info, "1. Started process");
    logging.WriteO(SHSAx.Logger.LogType::Info, "2. Finding inventory");
    logging.WriteO(SHSAx.Logger.LogType::Info, "3. Check inventory");
    logging.WriteO(SHSAx.Logger.LogType::Warn, "3. Batch no. not specified");
    logging.WriteO(SHSAx.Logger.LogType::Info, "3. Allocating batch BT00001");
    logging.WriteO(SHSAx.Logger.LogType::Info, "4. Posting oprder");

    logging = new SHSAx.Logger.AxLogging("ThisIsMyLog");
    logging.WriteO(SHSAx.Logger.LogType::Debug, "THIS IS MINE...!!!!!! DEBUG");
    logging.WriteO(SHSAx.Logger.LogType::Error, "THIS IS MINE...!!!!!! ERROR");
    logging.WriteO(SHSAx.Logger.LogType::Fatal, "THIS IS MINE...!!!!!! FATAL");
    logging.WriteO(SHSAx.Logger.LogType::Info, "THIS IS MINE...!!!!!! INFO");
    logging.WriteO(SHSAx.Logger.LogType::Warn, "THIS IS MINE...!!!!!! WARN");

    logging = new SHSAx.Logger.AxLogging("ShashiTest");
    logging.WriteO(SHSAx.Logger.LogType::Debug, "My Test - Debug");
    logging.WriteO(SHSAx.Logger.LogType::Error, "My Test - Error");
    logging.WriteO(SHSAx.Logger.LogType::Fatal, "My Test - Fatal");
    logging.WriteO(SHSAx.Logger.LogType::Info, "My Test - Info");
    logging.WriteO(SHSAx.Logger.LogType::Warn, "My Test - Warn");

}

I called these methods from a job to show the results. Note that this is a Server side solution. If you want this to be a client side then you may have to deploy the DLL to every client PC. For me calling a method on the server works well and all logs can be written on the server side.
You could dynamically create lognames based on the user, and then create an appender to capture only those messages.

Once done restart the AOS service and run the job.
MyFirstLoggerAx000001.log

2013-06-04 09:05:23,655 [71] INFO  DynamicsAx_Logging this is an info
2013-06-04 09:05:23,655 [71] DEBUG DynamicsAx_Logging this is a debug
2013-06-04 09:05:23,655 [71] ERROR DynamicsAx_Logging this is an Error
2013-06-04 09:05:23,655 [71] FATAL DynamicsAx_Logging this is an Fatal issue
2013-06-04 09:05:23,655 [71] WARN  DynamicsAx_Logging this is a Warning
2013-06-04 09:05:23,686 [71] WARN  Picking_list_posting 3. Batch no. not specified
2013-06-04 09:05:23,702 [71] FATAL ThisIsMyLog THIS IS MINE...!!!!!! FATAL
2013-06-04 09:05:23,718 [71] DEBUG ShashiTest My Test - Debug
2013-06-04 09:05:23,733 [71] ERROR ShashiTest My Test - Error
2013-06-04 09:05:23,733 [71] FATAL ShashiTest My Test - Fatal
2013-06-04 09:05:23,733 [71] INFO  ShashiTest My Test - Info
2013-06-04 09:05:23,733 [71] WARN  ShashiTest My Test - Warn

MyFirstLoggerAx000002.log

2013-06-04 09:05:23,702 [71] FATAL ThisIsMyLog THIS IS MINE...!!!!!! FATAL
2013-06-04 09:05:23,718 [71] DEBUG ShashiTest My Test - Debug
2013-06-04 09:05:23,733 [71] ERROR ShashiTest My Test - Error
2013-06-04 09:05:23,733 [71] FATAL ShashiTest My Test - Fatal
2013-06-04 09:05:23,733 [71] INFO  ShashiTest My Test - Info
2013-06-04 09:05:23,733 [71] WARN  ShashiTest My Test - Warn

MyFirstLoggerAx__PickList.log

2013-06-04 09:05:23,686 [71] WARN  Picking_list_posting 3. Batch no. not specified

Log result:

NOTE: to pick up the new C# project in X++ you may have to restart the AOS service. Also because we are reloading the configuration every time we call the logging class, there will be overheads. It will nice if the config file doesnt need to be changed.

Hopefully this can be turned into a more streamlined approach and we can get a common class / interface that we all can use to do some logging.

X++ date addition issue


I came across this issue with adding dates to negative integers. Basically the issue is that you cant add a negative value to a date variable (but you can subtract a positive integer).
i.e. if x = -5, and date1 is 01/01/2012,
and you do date1 = date1 + x,
this results in a runtime error which says “Error executing code”. This error is not even caught by the catch block.

Following is a job i wrote to prove this.


static void DateOperatorIssue(Args _args)
{
    Date date1,date2,date3;
    int dateDiff;
    ;
    try
    {
        date1 = mkdate(01,01,2013);
        date2 = mkdate(10,01,2013);
        date3 = mkDate(15,01,2013);
        dateDiff = date3 - date2;
        info(strfmt("Date diff is: %1", dateDiff)); //dateDiff = 5
        date1 += dateDiff;                          //This works (You can only add positive integers, or subtract positive integers)
        info(strfmt("New date is: %1", date1));
        dateDiff = date2 - date3;
        info(strfmt("Date diff is: %1", dateDiff)); //dateDiff = -5
        date1 += dateDiff;                          //Runtime error: Error executing code: Wrong type of argument for conversion function.
        info(strfmt("New date is: %1", date1));
    }
    catch(Exception::Error)
    {
        error('ooooops');                           //This is never reached
}
}

Walkthrough: Calling the Query Service with a Dynamic Query [AX 2012]


Ax2012 exposes the query service, and makes the queries available directly by a developer. these queries are static, Dynamic or User defined. Query Service [AX 2012]

Static queries are the simplest ones, all you need to do is to pass the query name, and you get the dataset in return. (Walkthrough: Calling the Query Service with a Static Query [AX 2012]) Works like a charm, except when you may want to filter on it (Lets say we want all items that are BOM types). You could filter the dataset in c# or your language of choice, but what if there are over 2000 items ?

Thats where Dynamic queries come in. There is a bit more work involved in it, but its not too much.

Firstly we need to create 2 classes. One which holds the query, and we add our ranges to it, and the second class which is a DataContract / Argument class which contains the parameters we define for the filtering. (or any special process for that matter)

In this example we will look into using the Query InventTable

1. Create the DataContract / Arguments class

We will create a class called MyInventTableQueryBuilderArgs which extends AifQueryBuilderArgs Class, and will be decorated with the DataContractAttribute, and the parmMethods with the DataMemberAttribute. This tehn exposes this class to the QueryService

[DataContractAttribute]
public class MyInventTableQueryBuilderArgs extends AifQueryBuilderArgs
{
ItemType itemType;
}

ItemType is what we will use as a filter. In order for it to be available in the QueryService, is to create a parm method
[DataMemberAttribute]
public ItemType parmItemType(ItemType _itemType = itemType)
{
itemType = _itemType;
return itemType;
}

2. Create the QueryBuilder class
Create a class called MyInventTableQueryBuilder which extends the AifQueryBuilder class. Add the MyInventTableQueryBuilderArgs as a property of the class, and use the setArgs to populate this object.
public class MyInventTableQueryBuilder extends AifQueryBuilder
{
MyInventTableQueryBuilderArgs args;
}

//Used internally to access the args object
private MyInventTableQueryBuilderArgs getArgs()
{
return args;
}

This class also requires the initialize method to be overridden, and this is where we will populate the query ranges using the args object

public void setArgs(AifQueryBuilderArgs _args)
{
if(!_args
|| classIdGet(_args) != classNum(MyInventTableQueryBuilderArgs))
{
throw error("@SYS95118");
}
args = _args;
}

[SysEntryPointAttribute]
public void initialize()
{
QueryBuildDataSource qbds;
query = new Query(queryStr(InventTable));

qbds = query.dataSourceTable(tableNum(InventTable));

SysQuery::findOrCreateRange(qbds, fieldNum(InventTable, ItemType)).value(SysQuery::value(this.getArgs().parmItemType()));

queryRun = new QueryRun(query);
}
Compile the 2 classes and then run the incremental compile

Now for the better half – Visual studio

Create a project of your choice (I am using a Console application for simplicity), and in the language of your choice (I am choosing c#)
Add a Web service reference (Right click reference and choose Add Service reference)
In the URL enter the following URL (replace with the server where Ax is installed. The port 8101 can be replaced based on the installation port if different
http://AOSserverName:8101/DynamicsAx/Services/QueryService
also in the same screen (after clicking GO), enter the namespace as MyQueryService

In the Main Method of the Program class, write the following code:

static void Main(string[] args)
{
int records = 0;
using (MyQueryService.QueryServiceClient client = new MyQueryService.QueryServiceClient())
{
MyQueryService.Paging paging = null;

MyQueryService.MyInventTableQueryBuilderArgs queryArgs = new MyQueryService.MyInventTableQueryBuilderArgs();
queryArgs.parmItemType = MyQueryService.ItemType.Item;

DataSet dataset = client.ExecuteDynamicQuery(“MyInventTableQueryBuilder”, queryArgs, ref paging);
if (dataset.Tables.Contains(“InventTable”))
{
DataTable datatable = dataset.Tables[“InventTable”];
foreach (DataRow row in datatable.Rows)
{
records++;
string itemId = (string)row[“ItemId”];
Byte itemType = (Byte)row[“ItemType”];
string dataAreaId = (string)row[“DataAreaId”];
Console.WriteLine(“Item: {0}, Type: {1}, Company: {2}”, itemId, itemType, dataAreaId);
}
}
}
Console.WriteLine(“Records: {0}”, records);
}

The code should now be good to run. For my installation and data, i get the following output:

Item: Office-Chair, Type: 0, Company: ceu
Item: Pack-Ribbon, Type: 0, Company: ceu
Item: SCPS_CSCL, Type: 0, Company: ceu
Item: SCPS_CSCR, Type: 0, Company: ceu
Item: SCPS_PSCL, Type: 0, Company: ceu
Item: SCPS_PSCR, Type: 0, Company: ceu
Item: SCPS_UPSCL, Type: 0, Company: ceu
Item: SCPS_UPSCR, Type: 0, Company: ceu
Item: WEE B1190, Type: 0, Company: ceu
Item: WMFlat32, Type: 0, Company: ceu
Item: WMFlat45, Type: 0, Company: ceu
Item: Work-Shirt, Type: 0, Company: ceu
Records: 298
Press any key to continue . . .

NOTE: If you do get an error regarding the message size being too big, then edit the app.config to make the
maxReceivedMessageSize="9999999" in the netTcpBinding element

Dynamics Ax Debug logging (File based)


The need for logging in Ax for me came from the .Net world of using Debug.Writeline , and then looking up the debug values from sysInternals dbgview.
However, to make a very quick and just basic configurable logging, i went to write the logs into a file.
This is a quick and dirty way to add logging capabilities into Dynamics ax.

The project can be found at Codeplex: http://axug.codeplex.com/SourceControl/changeset/view/82084#1932052
To go on with the current logging, following is the code signature of the method to be called for passing the logging message

public static void LogMessage(str _processId, str _text = '', int _step = 0)

and logging can be called by

Logging::LogMessage("Testing Logging","Logging is great :)", 0);

The Macro values define if the logging capability is turned on, and also the filename (So it can be configured from the usr layer / production site)

I have tested this with Ax 2012 and it has worked pretty well so far, and i do not think Ax2009 should have an issue with it. This logging is so far restricted to file based logging, but I am looking at incorporating other forms like Debug.WriteLine, and will further look into incorporating log4Net (however i have a strong suspicion that this will be limited to server side, and only for Ax 2012).

Hope this brings enough joy to you out there.
Happy Logging

NOTE: before you run the test class in the project, please make sure to edit the Macro to specify the folder path (This can be set to a local directory, but please be advised, this method is “called from”)

UPDATE:

I have now updated the code to include 2 more logging features. Along with that made changes to the method to include the type of message being passes (i.e. warning, information or error. We shall see why)

Change set: http://axug.codeplex.com/SourceControl/changeset/82210

1. Windows Event logging. – This enables you to you write into the Windows Event log directly. The log type (info , error, warning) will correlate directly to the log icons in the event viewer 🙂

2. Debug log: The most simplistic logging with .Net’s System.Diagnostics.Debug.WriteLine is now included. Although i have seen that this also writes to the event log 😦 (as information only, probably something that is handled from x++ itself, so may not be a good idea to have it in a production server where it manages to clutter the event viewer)

Adding Address to Vendor / Customer / Contact info [AX2012]


The msdn article How To: Update a Postal Address for a Contact Person [AX 2012] talks about adding a contact info, and an address associated with this.

On further reading of the Implementing the Global Address Book Framework (White paper) [AX 2012] I found that in a similar way you can use the CustomerEntity or VendorEntity to create addresses and electronic address.

However, to create an electronic address inside the postal address (LogisticsPostalAddress) you will have to use the LogisticsElectronicAddressEntity class to create the electronic address, after you modify the static method createElectronicAddress to accept parent location. (If you create the postal address as per the document, you will have the parent location with you)

Iterate through AOT tree


I came across this piece of code which was required to check the AOT elements for certain things.
This iteration should work fine as long as all the developments done show up in your session.
The best way is to restart the AOS and run it to make sure the utilElements have all new objects

UtilElements e;
UtilEntryLevel utilLevel = global::currentAOLayer();
TreeNode treeNode;
;
while select e
where e.utilLevel == utilLevel
&& !(e.recordType == 37 //SharedProjects
|| e.recordType == 78) //WebListDef
{
//treeNode = xUtilElements::getNodeInTree(e);
treeNode = xUtilElements::getNodeInTree(xUtilElements::parentElement(e));
//DO SOMETHING WITH THE treeNode HERE.
}

Ax bug in X++ join statement – Compile error


So i have found a bug in Ax relating to join statement.
Based on the Image, you can see that i am joining 2 tables, and setting a where statement for the first one to a tableId
All 3 should give me the same result.
However i get a compile error in the 3rd statement. It involves around checking the RefTableId field of Table 2, and comparing it to a tablenum method.

Ax X++ select statement join bug
Ax X++ select statement join bug

The 1st method uses the same functions, but all the where clauses are after the join statements. This however can get ugly (unreadable) when there are a lot of where clauses as you would like to have them grouped up.
//Join scenario 1
select firstonly table2
join table1
where table2.MyRefTableId == tablenum(Table2)
&& table1.SomeId == "ID00001";

The 2nd method of select statements groups the where clauses nicely. However to do that, I have to use the tablename2Id function instead.
//Join scenario 2
select firstonly table2
where table2.MyRefTableId == global::tableName2Id(tablestr(Table2))
join table1
where table1.SomeId == "ID00001";


The 3rd method is the culprit, for which i have no explanation at the moment. Anyone want to shed some light on this?
//Join scenario 3
select firstonly table2
where table2.MyRefTableId == tablenum(Table2)
join table1
where table1.SomeId == "ID00001";

This gives me a “Syntax error”