Monday, December 30, 2019

Index consideration for date effective tables

it is a best practice to add ValidTo to the ValidTimeStateKey index
and on the Properties tab (of the ValidTo field in the index) to
select Yes from the Included Column.

image

[see: http://dev.goshoom.net/en/2012/04/included-columns-in-ax2012]

but if it is a clustered index see this:

Performance considerations when designing valid time state tables
To help improve performance of valid time state tables, you should index them correctly.
Valid time state tables are modeled with an alternate key that includes the ValidFrom column.
In some models, the ValidTo column may have also be included in the alternate key,
but this is not necessary for uniqueness, and it should be removed from the alternate key constraint.
If the ValidFrom column is a key column of the clustered index,
the ValidTo column should not also be a key column of the clustered index
.
If the ValidFrom column is a key column of a non-clustered index,
the ValidTo column should be made an included column in the non-clustered index,
which provides coverage for range queries that involve both ValidTo and ValidFrom columns.

for examples see where the index is a clustered index see:
\Data Dictionary\Tables\LogisticsPostalAddress

image

or see
\Data Dictionary\Tables\HcmPositionWorkerAssignment

image

Friday, December 6, 2019

SysExtension framework for Dynamics Ax example

The benefits of using this new extension model are that the base and derived classes are decoupled, and it takes less code to extend the capability of the Microsoft Dynamics AX application.

The getClassFromSysAttribute method works by searching through the classes that are derived from the our base class (EF_ExtFrameworkSample6) until it finds a class that has matching attribute

The input value of the attribute class can be anything, an enum, a string, an integer. For this example I used the class name as input.

1) Create the attribute class

As you see I added in the attribute class also a generic static method that can be used by any extension that might use this attribute

class EF_ClassNameAttribute extends SysAttribute
{
    ClassName className;
}
public void new(ClassName _className)
{
    super();
    className = _className;
}
public ClassName parmClassName(ClassName _className = className)
{
    className = _className;

    return className;
}
//this method should be in the factory class, not here.
//I took the liberty to put it here becuse I might want to share it with multiple factory classes.
public static Object getClassFromSysAttribute(ClassName _baseClassName, ClassName _className)
{
    EF_ClassNameAttribute attr;
    Object cl;

    attr = new EF_ClassNameAttribute(_className);
    cl = SysExtensionAppClassFactory::getClassFromSysAttribute(_baseClassName, attr);

    if (!cl)
    {
        throw error(Error::wrongUseOfFunction(_baseClassName));
    }

    return cl;
}



2) Create the base class

the base class does not need to be abstract, but is a good practice

abstract class EF_ExtFrameworkSample6
{
    Name name;
    MethodName functionName;
}
abstract protected void init()
{
}
public void run()
{
    this.init();
    info(strFmt('Hello my name is %1', name));
    info(strFmt('the class that run is %1', functionName));
}


3) Create the Extensions


[EF_ClassNameAttribute(classStr(EF_ExtFrameworkSample6_1))]
class EF_ExtFrameworkSample6_1 extends EF_ExtFrameworkSample6
{
}
protected void init()
{
    name = 'Pippo';
    functionName = funcName();
}
[EF_ClassNameAttribute(classStr(EF_ExtFrameworkSample6_2))]
class EF_ExtFrameworkSample6_2 extends EF_ExtFrameworkSample6
{
}
protected void init()
{
    name = 'Topolino';
    functionName = funcName();
}


4) Create the Factory class


class EF_ExtFrameworkSample6Factory
{
}
protected void new()
{
}
public static EF_ExtFrameworkSample6 newFromClassName(ClassName _className)
{
    return EF_ClassNameAttribute::getClassFromSysAttribute(classStr(EF_ExtFrameworkSample6), _className);
}
public static EF_ExtFrameworkSample6 newFromSample6_1()
{
    return EF_ExtFrameworkSample6Factory::newFromClassName(classStr(EF_ExtFrameworkSample6_1));
}
public static EF_ExtFrameworkSample6 newFromSample6_2()
{
    return EF_ExtFrameworkSample6Factory::newFromClassName(classStr(EF_ExtFrameworkSample6_2));
}
//just for trials...
public static void main(Args _args)
{
    EF_ExtFrameworkSample6 cl;
    cl = EF_ExtFrameworkSample6Factory::newFromSample6_1();
    cl.run();
}

image

One side note, The extension framework cache so every time you do some changes during development clear the cache :

static void JobEF_SysExtensionCache(Args _args)
{
    SysExtensionCache::clearAllScopes();
}

Tuesday, September 24, 2019

Maintain Fast SQL Operations avoid set based operation to fall back to row by row operation using skip methods in Dynamics ax

Sometimes when importing large number of records (Eg. millions of lines), the quickest way is to use:

  • insert_recordset
  • update_recordset
  • delete_from

But these set base operation will roll back to row-by-row operation when either of the following condition is true:
  • it is not an SQL table (Eg. temporary table)
  • Database log is enabled for the table
  • Alert is setup for this table
  • Record level security is enabled for the table
  • AOSValidation method is overwritten
  • when using insert_recordset
    • the .insert() method is overwritten
  • when using update_recordset
    • the .update() method is overwritten
  • when using delete_from
    • the .delete() method is overwritten
    • DeleteAction is defined

To counter this, you can call a number of skip* methods:

  • common.skipDataMethods(true) will skip the insert/update/delete methods
  • common.skipDeleteMethod(true) will skip the delete method
  • common.skipDeleteActions(true) will not execute the delete actions
  • common.skipEvents(true) if alert is setup

image

see: https://docs.microsoft.com/en-us/dynamicsax-2012/developer/maintain-fast-sql-operations

I saw an example in this post: https://dynamicsuser.net/ax/f/developers/76285/update_recordset

public server static void MyCustomMethod(SalesLine _salesLine)
{
    salesLine           salesLineUpd;
    InventDim           inventDim;
    
  if ( _salesLine.MyField )
  {
    inventDim = _salesLine.inventDim();

    salesLineUpd.skipDataMethods( true );

    ttsBegin;

    update_recordSet salesLineUpd
    setting PER_ChairSerialNo = inventDim.inventSerialId
    where salesLineUpd.SalesId == _salesLine.SalesId
    && salesLineUpd.RecId != _salesLine.RecId; // update every other line with serial id

    ttsCommit;
  }


I also saw in this other post some suggestions regarding database log
http://www.artofcreation.be/2014/08/11/what-you-should-know-about-database-logging-functional-impact/

If you really want to activate database logging but you have code that need to do a set-based operation, you can get around this issue by using the skipDatabaseLog method in combination with the other skip* methods.

However, in my opinion it is better not to use database log in the first place. So these are my recommendations about database logging:

  • Do not use it.
  • If you do use it, make sure it is for a good reason and document why.
  • Do not use database logging because you do not trust your employees or as a form of “security”.
  • When activating the database log for a table, pay close attention to the TableGroup property of the table. It is fairly safe to activate the database log on tables with table group Main, Group and Parameter. Activating it for other groups such as Transaction, TransactionHeader, TransactionLine, WorksheetHeader and WorksheetLine is usually bad.
  • If you are a consultant, capture the need for database logging in the analysis phaseand set this up in your DEV/TST/ACC/… environments as early as possible.
  • Do not simply activate database logging in a production environment and expect everything to go well, test it first in an other environment as if it were a code change

Monday, September 9, 2019

Simulated Multiple Inheritance fox x++ in Dynamics Ax

We all know that in X++, a class can only extend one class; multiple inheritance is not supported in X++ and to overcome that we can use Interfaces. What I never saw is an example of multiple inheritance in X++. Instead I saw someone saying it’s a bug! I don’t know if was serious or a was making a joke…

Let’s start making things clear:

In Multiple inheritance, one class can have more than one superclass and inherit features from all its parent classes. As shown in the below diagram, class C inherits the features of class A and B.

Most recent languages like C# support only simple inheritance. They don't have multiple inheritance because their designers had to choose between have it in and have all the problems it comes with, or get it out of the language putting away all those problems, and introduce a versatile and less problematic substitute like interfaces and interface inheritance.
Multiple inheritance has a pathological problem:

Imagine, we want to have a class Child which inherits from Parent classes ParentA and ParentB. Both classes have the same methods MethodA and MethodB.
Now, when we instantiate the Class Child then calling MethodA will confuse the compiler that does not know from which class MethodA should be called.

The traditional way to emulate multiple inheritance with interface inheritance.

Let us see how to simulate multiple Inheritance in X++

image

first we create the interfaces

interface EF_IMultipleInheritaceBaseClass1
{
}
public void x()
{
}
public void y()
{
}

interface EF_IMultipleInheritaceBaseClass2
{
}
public void z()
{
}

next we can create two classes that implement those interfaces

class EF_MultipleInheritaceBaseClass1 implements EF_IMultipleInheritaceBaseClass1
{
}
public void x()
{
    info(funcName());//it shows the class and method name
}
public void y()
{
    info(funcName());
}

class EF_MultipleInheritaceBaseClass2 implements EF_IMultipleInheritaceBaseClass2
{
}
public void z()
{
    info(funcName());
}

now we are ready to create our abstract class that implements the two interfaces

abstract class EF_MultipleInheritaceBaseClass1And2 implements EF_IMultipleInheritaceBaseClass1, EF_IMultipleInheritaceBaseClass2
{
    EF_IMultipleInheritaceBaseClass1 class1;
    EF_IMultipleInheritaceBaseClass2 class2;
}
protected void new()
{
    class1 = classFactory.createClass(classNum(EF_MultipleInheritaceBaseClass1));
    class2 = classFactory.createClass(classNum(EF_MultipleInheritaceBaseClass2));
}
public void x()
{
    class1.x();
}
public void y()
{
    class1.y();
}
public void z()
{
    class2.z();
}

finally we can extend our abstract class and bingo, we simulated multiple inheritance:

class EF_MultipleInheritaceClass1And2 extends EF_MultipleInheritaceBaseClass1And2
{
}
public static EF_IMultipleInheritaceClass1And2 construct()
{
    return new EF_IMultipleInheritaceClass1And2();
}
public static void main(Args _args)
{
    EF_IMultipleInheritaceClass1And2 class1And2;
    class1And2 = EF_IMultipleInheritaceClass1And2::construct();
    class1And2.x();
    class1And2.y();
    class1And2.z();
}

Convert a Dynamics Enum to a C# generic collection

When you need to consume a Dynamics Ax Enum type in a .Net project you should make your project independent from the internal structure of Dynamics and certainly you do not want to create a static copy of your Dynamics Enum in your .Net project.

In order to achieve that I taught to expose the Dynamics Enum as a generic List.

To do that and adhere to the DRY principle (do not repeat yourself) I made an extension method in C# that converts any Enum in a generic list.

first you need to create a POCO class, a data transfer object

namespace MydDynamicsIntegration.Models
{
    public class AxEnumDefinition
    {
        public int Value { get; set; }
        public string Name { get; set; }
        public string Label { get; set; }
    }
}

now here the extension method

using System;
using System.Collections.Generic;
using MydDynamicsIntegration.Models;

namespace MydDynamicsIntegration.DynamicsCommon
{
    public static class AxGetValuesExtensions
    {
        public static List<AxEnumDefinition> FromAxToColl<T>(this T eEnum) where T : struct, IConvertible
        {
            var enumDefinitions = new List<AxEnumDefinition>();

            Type t = typeof(T);

            if (!t.IsEnum)
                throw new ArgumentException("T must be an enumerated type");

            try
            {
                SysDictEnum sysDictEnum = SysDictEnum.newName(t.Name);

                for (int i = 0; i < sysDictEnum.values(); i++)
                {
                    enumDefinitions.Add(new AxEnumDefinition()
                    {
                        Value = sysDictEnum.index2Value(i),
                        Name = sysDictEnum.index2Name(i),
                        Label = sysDictEnum.index2Label(i)
                    });
                }
            }
            catch
            {
                //
            }

            return enumDefinitions;
        }
    }
}

now you can use it like this

use example: 
return new HcmDiscussionStatus().FromAxToColl();
OR to a specific value
Status = new AxEnumDefinition()
   {
      Value = Global.enum2int(discussion.status),
      Name = Global.enum2Value(discussion.status),
      Label = discussion.status.FromAxToColl().Where(x => x.Value == Global.enum2int(discussion.status)).Select(x => x.Label).FirstOrDefault()
   }
OR 
Status = discussion.status.FromAxToColl().FirstOrDefault(x => x.Value == Global.enum2int(discussion.status))

Thursday, September 5, 2019

Attach documents in Dynamics Ax programmatically in x++ code. different options

I had to do some custom development to upload/Attach document in Ax programmatically from a webApi. So I did some research on what is the best way to it.

here I share the different options I found to do it.

OPTION 1
USE \Classes\DocuActionArchive\add (it extends DocuActionFile)

NOTE, is the preferred way, but doesn't let you customize anything. the description of the file is the file name

static void JobEF_DocumentInsert(Args _args)
{
    DocuType docuType;
    DocuTypeId docuTypeId;
    DocuRef docuRef;
    DocuValue docuValue;
    DocuActionArchive archive;

    Filename filename;

    HcmDiscussion hcmDiscussion;

    docuTypeId = 'AppraisalEnrico';
    hcmDiscussion = HcmDiscussion::findByDiscussionWorker('xx', HcmWorker::findByPersonnelNumber('xx').RecId);

    filename = @'\\...\Myfile.PNG';

    if ( ! hcmDiscussion ) return;

    docuType = DocuType::find(docuTypeId);

    ttsBegin;

    docuRef.RefCompanyId = hcmDiscussion.DataAreaId;
    docuRef.RefTableId   = hcmDiscussion.TableId;
    docuRef.RefRecId     = hcmDiscussion.RecId;
    docuRef.TypeId       = docuType.TypeId;
    docuRef.insert();

    archive = new DocuActionArchive();

    archive.add(docuRef, filename);

    ttsCommit;
}

OPTION 2
USE \Classes\DocuActionFile\insertDocuValue

Note: seems quite good option. you can't use this method directly as it is an abstract class, use \Classes\DocuActionArchive\

static void JobEF_DocumentInsert2(Args _args)
{
    DocuType docuType;
    DocuTypeId docuTypeId;
    DocuRef docuRef;
    DocuValue docuValue;
    DocuActionArchive archive;

    Filename filename;

    HcmDiscussion hcmDiscussion;

    docuTypeId = 'AppraisalEnrico';
    hcmDiscussion = HcmDiscussion::findByDiscussionWorker('xx', HcmWorker::findByPersonnelNumber('xx').RecId);

    filename = @'\\...\Myfile.PNG';

    if ( ! hcmDiscussion ) return;

    docuType = DocuType::find(docuTypeId);

    ttsBegin;

    docuRef.RefCompanyId = hcmDiscussion.DataAreaId;
    docuRef.RefTableId   = hcmDiscussion.TableId;
    docuRef.RefRecId     = hcmDiscussion.RecId;
    docuRef.TypeId       = docuType.TypeId;
    docuRef.Name         = strFmt('prova - %1', Docu::getFileName(filename));
    docuRef.insert();

    archive = new DocuActionArchive();

    archive.insertDocuValue(docuRef, filename);

    ttsCommit;
}

OPTION 3
NOTE: is using Docu::insertFile. more complex.

static void JobEF_DocumentInsert1(Args _args)
{
    DocuType docuType;
    DocuTypeId docuTypeId;
    DocuRef docuRef;
    DocuValue docuValue;
    DocuActionArchive archive;

    Filename filename;
    DocuValueFile file;
    BinData binData;
    boolean fileLocked;

    HcmDiscussion hcmDiscussion;
    #File
    
    //done like: \Classes\TrvImportReceiptsBatch\processFolder
    DocuValueFile getDocuValueFile(FileName _fullPathOfFile)
    {
        binData = new BinData();

        new FileIOPermission(_fullPathOfFile,'r').assert();
        // BP Deviation documented
        binData.loadFile(_fullPathOfFile);
        CodeAccessPermission::revertAssert();
        return binData.getData();
    }
    
    //done like: \Data Dictionary\Tables\DocuValue\Methods\writeDocuValue
    DocuValueFile getDocuValueFile1(FileName _fullPathOfFile)
    {
        binData = new BinData();
        if (isRunningOnServer())
        {
            // Assert permission and get the temp filename
            new FileIOPermission(_fullPathOfFile,#io_read).assert();
            // BP deviation documented
            fileLocked = WinApiServer::fileLocked(_fullPathOfFile);
            CodeAccessPermission::revertAssert();
        }
        else
        {
            // BP deviation documented
            fileLocked = WinApi::fileLocked(_fullPathOfFile);
        }

        // Insert to database
         if (fileLocked)
         {
            info("@SYS72783");
         }
        else
        {
            // LoadFile demands read permission on the file
            new FileIOPermission(_fullPathOfFile, #io_read).assert();
            // BP deviation documented
            if (binData.loadFile(_fullPathOfFile)) //only works if file not locked
            {
                file = binData.getData();
            }
            CodeAccessPermission::revertAssert();
        }
        return file;
    }
    
    docuTypeId = 'AppraisalEnrico';
    hcmDiscussion = HcmDiscussion::findByDiscussionWorker('123', HcmWorker::findByPersonnelNumber('123').RecId);

    filename = @'\\...\Myfile.PNG';

    file = getDocuValueFile(filename);

    if ( ! hcmDiscussion ) return;

    docuType = DocuType::find(docuTypeId);

    ttsBegin;

    docuRef.RefCompanyId = hcmDiscussion.DataAreaId;
    docuRef.RefTableId   = hcmDiscussion.TableId;
    docuRef.RefRecId     = hcmDiscussion.RecId;
    docuRef.TypeId       = docuType.TypeId;

    docuValue = Docu::insertFile(docuRef, filename, file, true);
    docuRef.ValueRecId = docuValue.RecId;
    docuRef.Name = 'mia prova';

    docuRef.insert();

    ttsCommit;
}

OPTION 4
USE \Classes\DocumentFileHelper\attachDocumentAsUser

SEE EXAMPLE: \Data Dictionary\Tables\RetailDiscountCode\Methods\createBarCodeImage

private void createBarCodeImage()
{
    #define.AttachmentName('BarcodeImage.jpg')

    RetailSharedParameters sharedParams;
    BarcodeSetup barcodeSetup;
    str base64ImageString;
    container attachDocumentParams;
    DocuRef DocuRefTable;
    RecId docuRefRecId;

    if (this.BarCode)
    {
        // Get selected barcode
        select firstOnly BarcodeSetupId from sharedParams
        join RetailBarcodeMask, fontName, fontSize from barcodeSetup
        where sharedParams.barcodeSetupId == barcodeSetup.barcodeSetupId;

        // Get base64 string of barcode image.
        base64ImageString = RetailBarcodeManagement::getBarcodeJpegImageAsBase64String(this.BarCode, barcodeSetup.fontName, barcodeSetup.fontSize);

        // Attach barcode image to the discount code.
        attachDocumentParams = [0, base64ImageString, #AttachmentName, '', DateTimeUtil::utcNow(), this.TableId, this.RecId, curext(), DocuType::typeFile()];
        [docuRefRecId] = DocumentFileHelper::attachDocumentAsUser(attachDocumentParams);

        // Make attached image external.
        select forUpdate DocuRefTable where DocuRefTable.RecId == docuRefRecId;
        // set document description to 'Bar code'
        DocuRefTable.Name = "@RET3053";
        DocuRefTable.Restriction = DocuRestriction::External;
        DocuRefTable.update();
    }
}

OR see \Classes\TrvUnreconciledExpenseService\createAttachments

OR \Classes\TrvReceiptService\createReceiptHelper

private DocuRef createReceiptHelper(RefTableId _tableId, RefRecId _recId, DataAreaId _dataAreaId, str _name, str _documentName, str _documentContents)
{
    Filename documentName;
    FilePath documentUrl;
    RefRecId contentType;
    str documentFile;
    DocumentFileReceiveDate receiveDate;
    DocuTypeId docuTypeId;
    TrvReceiptsHelper trvReceiptsHelper = new TrvReceiptsHelper();
    container args;
    RecId createdRecId;
    DocuRef docuRef;

    [documentName, documentUrl, contentType, receiveDate, documentFile] = DocumentFileHelper::getValidateUnpackedDocumentFileData(
                                                                _documentName, "", "",
                                                                DateTimeUtil::utcNow(), _documentContents);

    docuTypeId = trvReceiptsHelper.getDocuTypeId();

    args = [contentType, documentFile, documentName, documentUrl, receiveDate, _tableId, _recId, _dataAreaId, docuTypeId];

    [createdRecId] = DocumentFileHelper::attachDocumentAsUser(args);

    if (createdRecId != 0)
    {
        docuRef = DocuRef::findRecId(createdRecId, true);

        if (_name != '')
        {
            docuRef.Name = _name;
        }
        else
        {
            docuRef.Name = "@SYS138348";
        }
        docuRef.update();
    }

    return docuRef;
}

NOTE to get the documentContents see: \Classes\TrvReceiptService\getDocumentContents

.... 
    // Get the receipt contents and serialize them.
    binData = DOCommonDocuUtils::GetDocuContent(docuRef);
    if (binData && conLen(binData.getData()) > 0)
    {
        documentContents = binData.base64Encode();
    }

    return documentContents;


------------------------------------------------

Finally I didn’t use none of them but I ended extending an existing class which gives me the advantage to code less (always use existing methods!) and also to upload all the files in a directory in one go. So from the WebApi I create a shared folder where I upload all the documents I want to be attached to an Ax record and my extended class is responsible to Attach the document.
When the process ends Ax deletes the shared folder.

class MYD_HcmDiscussionAttachmentHelper extends TrvImportReceiptsBatch
{
    HcmDiscussion hcmDiscussion;
    DocuTypeId docuTypeId;
}

//for trials only
public static void main(Args _args)
{
    FilePath dirPath;
    HcmDiscussion hcmDiscussion;

    MYD_HcmDiscussionAttachmentHelper cl = new MYD_HcmDiscussionAttachmentHelper();

    dirPath = @'\\...\temp\000198';
    hcmDiscussion = HcmDiscussion::findByDiscussionWorker('000198', HcmWorker::findByPersonnelNumber('123').RecId);

    cl.parmDirPath(dirPath);
    cl.parmHcmDiscussion(hcmDiscussion);
    cl.parmDocuTypeId('AppraisalEnrico');
    cl.run();
}

boolean importFile(Filename _filename, DocuValueFile _file)
{
    DocuRef docuRef;
    DocuValue docuValue;
    boolean ret = false;

    if (hcmDiscussion)
    {
        ttsbegin;
            docuRef.RefCompanyId = hcmDiscussion.DataAreaId;
            docuRef.RefTableId   = hcmDiscussion.TableId;
            docuRef.RefRecId     = hcmDiscussion.RecId;
            docuRef.TypeId       = docuTypeId;

            docuValue = Docu::insertFile(docuRef, _filename, _file, true);
            docuRef.ValueRecId = docuValue.RecId;
            docuRef.Name = System.IO.Path::GetFileNameWithoutExtension(_filename);

            docuRef.insert();
        ttscommit;
        ret = true;
    }

    return ret;
}

public DocuTypeId parmDocuTypeId(DocuTypeId _docuTypeId = docuTypeId)
{
    docuTypeId = _docuTypeId;

    return docuTypeId;
}

public HcmDiscussion parmHcmDiscussion(HcmDiscussion _hcmDiscussion = hcmDiscussion)
{
    hcmDiscussion = _hcmDiscussion;

    return hcmDiscussion;
}

public void run()
{
    boolean result = true;
    try
    {
        super();
    }
    catch
    {
        result = false;
    }

    if (result)
    {
        new FileIOPermission(dirPath,'W').assert();
        System.IO.Directory::Delete(dirPath, true);
        CodeAccessPermission::revertAssert();
    }
}

Wednesday, September 4, 2019

utility method to get all table fields their Extended data type or Enum type and add them as parm methods in a class programmatically

I had to get the list of fields from a table with quite many fields to be used as properties in a class. quite and error prone task.

for example for CustTable you should have to do:

classDelaration
{
   CustAccount accountNum;
    CustInvoiceAccount invoiceAccount;
    CustGroupId custGroup;
   ….
}

I made this job that using a bit reflection does the work for you:

static void JobEF_TableGetFieldsListForProperties(Args _args)
{
    TableID tableID;
    DictTable dictTable;
    TableName tableName;
    FieldId fieldId;
    DictField dictField, dictFieldFrom;
    Common buffer;
    TableScope scope = TableScope::CurrentTableOnly;
    ExtendedDataTypeName fieldType;
    EnumName fieldEnum;
    FieldName fieldName;
    LabelString fieldTypeLabel, fieldEnumLabel;
    IdentifierName fieldDataType;
    TextBuffer txtb = new TextBuffer();
    int i;

    tableName = tableStr(CustTable);

    tableID = tablename2id(tableName);
    dictTable = new DictTable(tableID);

    if (!dictTable)
        throw error('invalid table');

    buffer = dictTable.makeRecord();

    fieldId   = dictTable.fieldNext(0, scope);

    while (fieldId && ! isSysId(fieldId))
    {
        dictField   = new DictField(tableID, fieldId);

        fieldType = Global::extendedTypeId2name(dictField.typeId());
        fieldEnum = Global::enumId2Name(dictField.enumId());
        fieldName = dictField.name();
        fieldTypeLabel = Global::extendedTypeId2pname(dictField.typeId());
        fieldEnumLabel = Global::enumId2pname(dictField.enumId());

        fieldDataType = fieldType ? fieldType : fieldEnum;

        if (i)
            txtb.appendText('\n');

        txtb.appendText(strFmt('%1 %2;', fieldDataType, MYD_Functions::stringLowerCaseFirst(fieldName)));

        i++;

        fieldId         = dictTable.fieldNext(fieldId, scope);
    }

    info(txtb.getText());
}

I have a helper class where I have all my utility methods, one is the one used in this code:

public static str stringLowerCaseFirst(str string)
{
    System.String myString, firstLetter;
    str firstLetterLwr, stringEnd, stringNew;

    if (strLen(string) < 2)
        return strLwr(string);

    myString = string;

    firstLetter = myString.Substring(0, 1);
    stringEnd = myString.Substring(1);
    firstLetterLwr = firstLetter.ToLowerInvariant();
    stringNew = strFmt('%1%2', firstLetterLwr, stringEnd);

    return stringNew;
    
    /*
    NOTE:
    you can do the same like \Classes\xppSource\parmMethod
    stringNew = strLwr(subStr(string,1,1))+subStr(string,2,strLen(string));
    I don't know which one is best. I come form C#
    */
}

I took it a step further and this other Job also auto generate the parm methods in a class for each table field.

To complete this task I used two Ax Foundation classes:

  • ClassBuild
  • XppSource

ClassBuild contains methods to programmatically create a class and methods

XppSource is the same class which you use behind the scene when you use shorcuts in the code editor or when you select a template under the script button in the editor toolbar.

static void JobEF_ClassMakeDataContractPerTblFields(Args _args)
{
    TableID tableID;
    DictTable dictTable;
    TableName tableName;
    FieldId fieldId;
    DictField dictField, dictFieldFrom;
    Common buffer;
    TableScope scope = TableScope::CurrentTableOnly;
    ExtendedDataTypeName fieldType;
    EnumName fieldEnum;
    FieldName fieldName;
    LabelString fieldTypeLabel, fieldEnumLabel;
    IdentifierName fieldDataType;
    TextBuffer txtb = new TextBuffer();
    int i;
    ClassName className;
    Source source;
    ClassBuild classBuild;
    XppSource xppSource;
    MethodName paramMethodName;
    str parmPrefixToFieldName;
    boolean addDataContractAttribute;

    //SET the table you want to get all fields from
    tableName = tableStr(CustTable);
    //SET the class you want the parm methods to be created
    className = classStr(MYDataContractClass);
    //SET optionally a prefix to all Table field names for the parm
    parmPrefixToFieldName = '';
    //SET if you want to add the [DataContractAttribute] attribute
    addDataContractAttribute = true;
    
    /*
    the hob will run and create a parm for each field of the table in the class.
    From the info log copy the parmeters and paste in the class declaration.
    */

    tableID = tablename2id(tableName);
    dictTable = new DictTable(tableID);


    if (!dictTable)
        throw error('invalid table');

    buffer = dictTable.makeRecord();

    fieldId   = dictTable.fieldNext(0, scope);

    while (fieldId && !isSysId(fieldId))
    {
        dictField   = new DictField(tableID, fieldId);

        fieldType = Global::extendedTypeId2name(dictField.typeId());
        fieldEnum = Global::enumId2Name(dictField.enumId());
        fieldName = strFmt('%1%2', parmPrefixToFieldName, dictField.name());
        fieldTypeLabel = Global::extendedTypeId2pname(dictField.typeId());
        fieldEnumLabel = Global::enumId2pname(dictField.enumId());

        fieldDataType = fieldType ? fieldType : fieldEnum;

        if (i)
            txtb.appendText('\n');

        txtb.appendText(strFmt('%1 %2;', fieldDataType, MYD_Functions::stringLowerCaseFirst(fieldName)));

        i++;

        classBuild = new ClassBuild(className);
        xppSource = new XppSource();
        source = xppSource.parmMethod(fieldDataType, fieldName);

        paramMethodName = strFmt('parm%1%2', parmPrefixToFieldName, strUpr(subStr(fieldName,1,1))+subStr(fieldName,2,strLen(fieldName)));
        
        if (addDataContractAttribute)
            source = strFmt('[DataContractAttribute]\n%1', source);
        
        classBuild.addMethod(paramMethodName, source);

        fieldId = dictTable.fieldNext(fieldId, scope);
    }

    info(txtb.getText());
}

I hope it helps!

DateTime conversion: utcdatetime to string and System.DateTime to utcdatetime

convert utcdatetime to string

static void JobEF_DateTimeToString(Args _args)
{
    utcDateTime myDate = DateTimeUtil::getSystemDateTime();
    
    //OPTION1:
    info(DateTimeUtil::toStr(myDate));
    //OPTION2:
    info(DateEffectivenessCheck::queryDate(myDate));
}

convert System.DateTime to utcdatetime

static void JobEF_DateTimeToUtcDatetime(Args _args)
{
    System.DateTime date1 = System.DateTime::get_Now();

    //OPTION1:
    utcdatetime date2 = CLRInterop::getAnyTypeForObject(date1);
    //OPTION2 (behind the scene, does the same as the above). better use this
    utcdatetime date3 = Global::clrSystemDateTime2UtcDateTime(date1);

    info(strFmt('%1', date2));
    info(strFmt('%1', date3));
}

Example of Date Filter in a form DataSource using SysQuery and DateEffectivenessCheck

The following code is an example of how to build a query range based on a Form Data Source, which displays only "daily" records.

In the example the form-dataSource is named DataSourceName is a date effective table. Depending on a check box only records should be displayed, which that are valid for todays date.

While it is quite a simple task, I want to remind how much easier and more reliable the code is if we use AX built in methods: in particular those two classes:

  • SysQuery
  • DateEffectivenessCheck
public void applyFilter()
{
    queryBuildRange qbr;

    qbr = sysQuery::findOrCreateRange(DataSourceName_ds.queryBuildDataSource(), fieldNum(DataSourceName, recId));

    if( !ShowExpiredCheckBox.checked())
    {
        qbr.value(
               strfmt(
                    DateEffectivenessCheck::queryRange(true,false,false),
                    DataSourceName_ds.queryBuildDataSource().name(), // queryBuildDataSource name
                    fieldstr(DataSourceName, ValidFrom), // table field from date
                    fieldstr(DataSourceName, ValidTo), // table field to   date
                    DateEffectivenessHelp::queryDate() // test date (MUST use the queryDate method for correct formatting), alternatively can use also: DateTimeUtil::toStr(DirUtility::getCurrentDateTime())
                 )                                              
                     );
    }
    else
    {
        qbr.value(SysQuery::valueUnlimited());
    }
}

how much more code would you need without using those two classes!!

Monday, September 2, 2019

c# extension method to retrieve documents from Dynamics Ax and expose them to a WebApi via a repository class

As an introduction let me show you a piece of X++ to get a document.

You can find much more if you google it.

static void JobEF_DocumentSearch(Args _args)
{
    Notes notes = "";
    DocuRef docuRefTmp;
    HcmDiscussion hcmDiscussion;
    DocuRefSearch docuRefSearchTmp;
    DocuTypeId docuTypeId;
    FilePath filePath, filePathGeneric, path;

    hcmDiscussion = HcmDiscussion::findByDiscussionWorker('000024', HcmWorker::findByPersonnelNumber('2002450').RecId);
    if ( ! hcmDiscussion ) return;


    //docuRefSearchTmp = DocuRefSearch::newCommon( hcmDiscussion );
    //filter per document type
    docuTypeId = 'Appraisal';
    docuRefSearchTmp = DocuRefSearch::newDocuTypeId(hcmDiscussion, docuTypeId);

    filePathGeneric = Docu::archivePath(curExt());
    filePath = DocuType::find(docuTypeId).ArchivePath;

    info(filePathGeneric);
    info(filePath);

    while ( docuRefSearchTmp.next() )
    {
        docuRefTmp = docuRefSearchTmp.docuRef();

        notes = docuRefTmp.docuValue().FileName;
        path = docuRefTmp.path();
        info(strFmt('%1 --- %2', path, notes));
    }
}

now let me show you how we can do the same from c# using proxy objects

to open a session i will use the code described in this post, read it first:
https://enricoariel.blogspot.com/2019/08/proxy-classes-for-net-interop-to-x.html

step1: create an extension class

using System;
using System.Collections.Generic;
using Microsoft.Dynamics.AX.Framework.Linq.Data;
using U23 = Microsoft.Dynamics.AX.ManagedInterop;
using U22 = Microsoft.Dynamics.AX.Framework.Linq.Data;

namespace MydDynamicsIntegration.DynamicsCommon
{
    public static class GenericRecord
    {
        public static List<DocuRefProperties> GetAttachments<T>(this T axTable, string docuTypeId = null) where T : Common
        {
            var ret = new List<DocuRefProperties>();

            var docuRefSearchTmp = docuTypeId == null ? DocuRefSearch.newCommon(axTable) : DocuRefSearch.newDocuTypeId(axTable, docuTypeId);

            while (docuRefSearchTmp.next())
            {
                DocuRef docuRefTmp = docuRefSearchTmp.docuRef();
                var docuref = new DocuRefProperties
                {
                    RecId = docuRefTmp.RecId,
                    ValueRecId = docuRefTmp.ValueRecId,
                    Path = docuRefTmp.path(),
                    FileName = docuRefTmp.docuValue().fileName(),
                    Description = docuRefTmp.Name,
                    TypeId = docuRefTmp.TypeId,
                    CreatedDateTime = (DateTime) docuRefTmp.CreatedDateTime
                };
                ret.Add(docuref);
            }

            return ret;
        }

        public class DocuRefProperties
        {
            public long RecId { get; set; }
            public long ValueRecId { get; set; }
            public string Path { get; set; }
            public string FileName { get; set; }
            public string Description { get; set; }
            public string TypeId { get; set; }
            public DateTime CreatedDateTime { get; set; }
        }
    }
}

Note: import the relevant proxy objects in your .Net project

step2: create the repository

in this example I made a repository for documents attached to HcmDiscussion

DiscussionRepository.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using MydDynamicsIntegration.DynamicsCommon;
using MydDynamicsIntegration.Models;
using MydDynamicsIntegration.Repositories.Interfaces;

namespace MydDynamicsIntegration.Repositories.Implementation
{
    public class DiscussionRepository : IAmDiscussionRepository
    {
        private const string AppraisalDocuTypeId = "Appraisal";
        public List<Attachment> GetAttachments(string discussionId, string personnelNumber)
        {
            using (var axSession = new AxSessionManager())
            {
                axSession.OpenConnection();

                var workerRecId = HcmWorker.findByPersonnelNumber(personnelNumber).RecId;
                var discussion = HcmDiscussion.findByDiscussionWorker(discussionId, workerRecId);

                return discussion.GetAttachments(AppraisalDocuTypeId).Select(attachment => new Attachment
                {
                    RecId = attachment.RecId,
                    ValueRecId = attachment.ValueRecId,
                    Path = attachment.Path,
                    FileName = attachment.FileName,
                    Description = attachment.Description,
                    TypeId = attachment.TypeId,
                    CreatedDateTime = attachment.CreatedDateTime
                }).ToList();
            }
        }       
    }
}

Attachment.cs:

using System;

namespace MydDynamicsIntegration.Models
{
    public class Attachment
    {
        public long RecId { get; set; }
        public long ValueRecId { get; set; }
        public string Path { get; set; }
        public string FileName { get; set; }
        public string Description { get; set; }
        public string TypeId { get; set; }
        public DateTime CreatedDateTime { get; set; }
    }
}

I omitted to copy the IAmDiscussionRepository, but is quite intuitive.

step3: use the repository in the WebApi controller class:

        [Authorize]
        [GET("api/worker/{personnelNumber}/discussions/{discussionId}/attachments/{attachmentRecId}")]
        [HttpGet]
        public HttpResponseMessage Attachment(string personnelNumber, string discussionId, long attachmentRecId)
        {
            var attachment = _discussionRepository.GetAttachment(discussionId, personnelNumber, attachmentRecId);

            return attachment.IsNullOrEmptyObject() ?
                Request.CreateErrorResponse(HttpStatusCode.NotFound, HttpStatusCode.NotFound.ToStringWithSpaces()) : 
                Request.CreateResponse(HttpStatusCode.OK, _attachmentMapping.Map(attachment));
        }

Note: in the webApi project I used Ninject and Attribute routing.

finally here the mapping class:

using System.Collections.Generic;
using System.IO;
using System.Linq;
using IDHGroup.DynamicsApi.CIT.Services.Interfaces;
using IDHGroup.DynamicsDataTransfer.CIT.Models.Dtos.RequestView;
using MydDynamicsIntegration.Models;

namespace IDHGroup.DynamicsApi.CIT.Services.Implementations
{
    public class AttachmentMappingService : IAmAttachmentMapping
    {
        public List<AttachmentViewDto> Map(List<Attachment> attachments)
        {
            return attachments.Select(MapAttachmentViewDto).ToList();
        }

        public AttachmentViewDto Map(Attachment attachment)
        {
            return MapAttachmentViewDto(attachment);
        }

        private static AttachmentViewDto MapAttachmentViewDto(Attachment attachment)
        {
            return new AttachmentViewDto
            {
                RecId = attachment.RecId,
                FilePath = Path.Combine(attachment.Path, attachment.FileName),
                Description = attachment.Description,
                TypeId = attachment.TypeId,
                CreatedDateTime = attachment.CreatedDateTime
            };
        }
    }
}

Search Projects where an Element Is Used in Dynamics Ax using x++ Reflection

today I need to search in which project an object was customized and ask for the relevant FDD. It is always a good practice to add some notes when you customize code, but not everybody does… So I found in the book Microsoft Dynamics AX 2012 R3 Development Cookbook on pag 333 a good recipe. But unfortunately the object I was looking for was in a deeper node and this recipe did not work.

Let me first bring the original recipe for Searching for an object in a development project from the book

In Dynamics AX, any development changes to the application normally have to be organized
in development projects. The same object could belong to one or more projects, but Dynamics
AX does not provide an easy way to determine which development projects a specific object
belongs to.
In this recipe, we will create a class to search for an object in the development projects.
The class is only for demonstration purposes, but it can be easily converted to a standalone
tool and integrated into the right-click menu.

In the AOT, create a new class with the following code snippet:

class EF_SearchProjectsElementIsUsed
{
}

public static void main(Args _args)
{
    EF_SearchProjectsElementIsUsed::TestDevProjectSearch();
}

static server void TestDevProjectSearch()
{
    EF_SearchProjectsElementIsUsed search;
    search = new EF_SearchProjectsElementIsUsed();
    search.find(UtilElementType::Table, tableStr(CustTable));
}

void find(UtilElementType _type, IdentifierName _name)
{
    TreeNode projects;
    ProjectNode project;

    projects = SysTreeNode::getSharedProject();
    if (!projects)
    {
        return;
    }

    project = projects.AOTfirstChild();

    while (project)
    {
        if (this.findChildren(
                project.loadForInspection(),
                _type,
                _name))
        {
            info(project.AOTname());
        }
        project = project.AOTnextSibling();
    }
}

private boolean findChildren(
            TreeNode _parent,
            UtilElementType _type,
            IdentifierName _name)
{
    TreeNode child;
    TreeNodeIterator iterator;
    #TreeNodeSysNodeType
    iterator = _parent.AOTiterator();
    child = iterator.next();

    while (child)
    {
        if (child.treeNodeType().id() == #NT_PROJECT_GROUP)
        {
            return this.findChildren(child, _type, _name);
        }
        else if (child.AOTname() == _name &&
                        child.treeNodePath() &&
                        child.utilElement().recordType == _type)
        {
            return true;
        }
        child.treeNodeRelease();
        child = iterator.next();
    }
    return false;
}
How it works...

In this recipe, we create a new class with several methods. The first method is
findChildren() and is used for a recursive search operation within the AOT node. It
accepts three parameters: a TreeNode object, an element type, and an element name. In
this method, we go through all the children of the TreeNode object and check whether any
of them match the provided element type and name. If any of the child nodes contain more
nodes within, we use the same findChildren() method to determine whether any of its
children match the element type and name.
The second method is named find() and is used for the actual search, for the given element
type and name. The method goes through all of the shared development projects and calls the
findChildren() method to determine whether the given element is in one of its nodes.
The class can be called from anywhere in the system, but in this recipe, to demonstrate how it
works, we create a new job, define and instantiate the class, and use the find() method to
search for the CustTable table in all the shared projects.

Mine enhanced recipe using a dialog and TreeNodeTraverser to get also child nodes containing the object

I also added a filter to search only in projects belonging to a specific layer. So I can just search to which project in cus layer the object belong. This speeds the search quite significantly.

class EF_SearchProjectsElementIsUsed
{
}
public static void main(Args _args)
{
    Dialog dialog;
    DialogField elementType, layer, elementName;
    UtilElementType type;
    IdentifierName name;
    UtilEntryLevel projectLayer;

    dialog = new Dialog('Search element in projects');

    elementName = dialog.addField(extendedTypeStr(IdentifierName));
    layer = dialog.addField(enumStr(UtilEntryLevel), 'Project Layer');
    elementType = dialog.addField(enumStr(UtilElementType), 'Type');
    dialog.addText("NOTE: setting the type will traverse the node and search in child nodes");

    elementName.control().mandatory(true);

    if (dialog.run() && elementName.value() != '')
    {
        type = elementType.value();
        name = elementName.value();
        projectLayer = layer.value();

        EF_SearchProjectsElementIsUsed::TestDevProjectSearch(type, name, projectLayer);
    }


}
static server void TestDevProjectSearch(UtilElementType _type, IdentifierName _name, UtilEntryLevel _projectLayer)
{
    EF_SearchProjectsElementIsUsed search;
    search = new EF_SearchProjectsElementIsUsed();
    search.find(_type, _name, _projectLayer);
}
void find(UtilElementType _type, IdentifierName _name, UtilEntryLevel _projectLayer)
{
    TreeNode projects;
    ProjectNode project;
    Set objectsInProject;
    SetEnumerator objectsInProjectEnumerator;
    str objectInProjectPath;

    projects = SysTreeNode::getSharedProject();
    if (!projects)
    {
        return;
    }

    project = projects.AOTfirstChild();

    setprefix(strFmt('These projects contain %1', _name));

    while (project)
    {
        /* I just want to search in cus layer, so is faster.
        to search all remove the condition */
        if (project.AOTLayer() != _projectLayer)
        {
            project = project.AOTnextSibling();
            continue;
        }

        objectsInProject = this.findChildren(
                                    project.loadForInspection(),
                                    _type,
                                    _name);

        if (objectsInProject.elements())
        {
            setprefix(project.AOTname());
            objectsInProjectEnumerator = objectsInProject.getEnumerator();

            while (objectsInProjectEnumerator.moveNext())
            {
                objectInProjectPath = objectsInProjectEnumerator.current();
                info(objectInProjectPath);
            }

        }
        project = project.AOTnextSibling();
    }
}
private Set findChildren(
            TreeNode _parent,
            UtilElementType _type,
            IdentifierName _name)
{
    TreeNodeTraverser tnt;
    TreeNode treeNode;
    UtilElements utilElements;
    TreeNodeType nodeType;
    boolean isRoot;
    str treePath;
    boolean nodeTypeIsUtilElement;
    boolean isTreePath;
    boolean showObject;
    Set objectsInProject;

    tnt = new TreeNodeTraverser(_parent);
    objectsInProject = new Set(Types::String);

    while (tnt.next())
    {
        treeNode = tnt.currentNode();
        nodeType = treeNode.treeNodeType();
        showObject = true;

        if (_type)
        {
            showObject = false;
            nodeTypeIsUtilElement = nodeType.isUtilElement();
            isTreePath = treeNode.treeNodePath() ? true : false;
            isRoot = nodeType.isRootElement();

            if (nodeTypeIsUtilElement && isTreePath && isRoot)
            {
                utilElements = treeNode.utilElement();//need the above conditions otherwise treeNode.utilElement() might fail
                if (utilElements.recordType == _type)
                    showObject = true;
            }
            //Debug::printDebug(strFmt('x: %1 - %2', showObject, treeNode.AOTToString()));
        }


        if (treeNode.AOTname() == _name && showObject)
        {
            objectsInProject.add(strFmt('%1 - (is root: %2)', treeNode.AOTToString(), nodeType.isRootElement()));
        }

        treeNode.treeNodeRelease();
    }
    return objectsInProject;
}

this how it would look like without setting the type. (note that it finds every object, tables forms, menu items… and also child objects like form datasources)

image

image

while setting the type it will filter per object type.

image

image

Wednesday, August 28, 2019

update record from c# Linq to Ax with impersonation runAs as a Active Directory Windows User

when you update a record you might want to store who made the last change

image

when you are within ax this is done automatically as long as you have set on your table the ModifyBy property to yes.

if you use Linq to Ax using .Net interop to X++ it becomes a bit more complex.

on the example you find here https://docs.microsoft.com/en-us/dynamicsax-2012/developer/code-example-linq-to-ax-from-csharp
they connect using this code

         // Logon to Dynamics AX.
         U23.Session axSession = new U23.Session();
         axSession.Logon(null, null, null, null);

which will cause that it will always mark the modifiedBy user to the AOT user.

there is a different way to open the session in C# which is axSession.LogonAs
so you can do:

U23.Session axSession = new U23.Session();
System.Net.NetworkCredential nc = new System.Net.NetworkCredential("ProxyUserID", "password");
var strUserName = nc.UserName;
axSession.LogonAs(strUserName.Trim(), "yourDomain.com", nc, "fch", "en-GB", null, null);

which works perfectly fine if you have the Active Directory username and password.

If you authenticate your client web site using forms authentication and the user enters his username and password you are fine, you have them in clear (just don’t tell it to a security expert…). Otherwise if you use for example windows authentication you need to start messing up with impersonation to pass the network credentials to you the API.

here an introduction: How To: Use Impersonation and Delegation in ASP.NET 2.0

it is quite a complex subject, so I decided to use a different approach and leverage the runAs method.

here the class I created:

//This class is useful when doing a record update from Linq to Ax. otherwsie it would take the AOS user as the user making the change.
class MYD_UpdateImpersonated
{
    Common common;
    UserId userId;
}
private void new()
{
}
public static MYD_UpdateImpersonated construct()
{
    return new MYD_UpdateImpersonated();
}
protected Common parmCommon(Common _common = common)
{
    common = _common;

    return common;
}
protected UserId parmUserId(UserId _userId = userId)
{
    userId = _userId;

    return userId;
}
public RecId update()
{
    container args;
    container result;
    container recordBuf;
    TableId tableId;
    RecId recId;

    RunAsPermission permission;

    recordBuf = buf2Con(common);
    args = [common.TableId, recordBuf, common.RecId];

    permission = new RunAsPermission(userId);
    permission.assert();

    // Need use the runas method; we want to run as the user to update records
    // BP deviation documented
    result = runAs(userId, classNum(MYD_UpdateImpersonated), staticMethodStr(MYD_UpdateImpersonated, updateAsUser), args);
    CodeAccessPermission::revertAssert();

    [recId] = result;
    return recId;
}
private static container updateAsUser(container _args)
{
    DictTable dictTable, dictTableBuf;
    Common common, commonBuf;
    TableId tableId;
    container recordBuf;
    RecId recId;

    [tableId, recordBuf, recId] = _args;

    dictTable = new DictTable(tableId);
    dictTableBuf = new DictTable(tableId);

    common = dictTable.makeRecord();
    commonBuf = dictTableBuf.makeRecord();

    con2Buf(recordBuf, commonBuf);

    ttsBegin;
    common.selectForUpdate(true);
    select common where common.RecId == recId;
    buf2Buf(commonBuf, common);
    common.update();
    ttsCommit;

    return [common.RecId];
}
public static server MYD_UpdateImpersonated newFromCommon(Common _common, UserId _userId)
{
    MYD_UpdateImpersonated updateImpersonated;

    updateImpersonated = MYD_UpdateImpersonated::construct();
    updateImpersonated.parmCommon(_common);
    updateImpersonated.parmUserId(_userId);

    return updateImpersonated;
}

now in c# you can create an extension class like this:

using System;
using System.Collections.Generic;
using Microsoft.Dynamics.AX.Framework.Linq.Data;
using U23 = Microsoft.Dynamics.AX.ManagedInterop;
using U22 = Microsoft.Dynamics.AX.Framework.Linq.Data;

namespace MydDynamicsIntegration.DynamicsCommon
{
    public static class GenericRecord
    {
        public static long UpdateRecord<T>(this T axTable, string windowsUser) where T : Common
        {
            var userInfo = AifPortUser::getAxaptaUser(windowsUser);// in the format  DOMAIN\USERNAME
            //if (!userInfo.isActiveDirecroyUser()) return 0;//if you prefer rather that an error is thrown from MYD_UpdateImpersonated class if the user is invalid
            var updateImpersonated = MYD_UpdateImpersonated.newFromCommon(axTable, userInfo.getUserId());
            return updateImpersonated.update();
        }
}

now we create our repository class like this:

public void UpdateDiscussion(Discussion discussion, string windowsUser)
{
    //see: https://enricoariel.blogspot.com/2019/08/proxy-classes-for-net-interop-to-x.html
    //on how to open a session inside a using statement
    using (var axSession = new AxSessionManager())
    {
        axSession.OpenConnection();

        var workerRecId = HcmWorker.findByPersonnelNumber(discussion.Worker).RecId;
        var hcmDiscussion = HcmDiscussion.findByDiscussionWorker(discussion.DiscussionId, workerRecId);

        hcmDiscussion.status = (HcmDiscussionStatus) discussion.Status.Value;
        hcmDiscussion.UpdateRecord(windowsUser);  
    }
}

finally here my discussion dto Poco classes

namespace MydDynamicsIntegration.Models
{
    public class Discussion
    {
        public string DiscussionId { get; set; }
        public AxEnumDefinition Status { get; set; }
        public string Worker { get; set; }
    }
}

namespace MydDynamicsIntegration.Models
{
    public class AxEnumDefinition
    {
        public int Value { get; set; }
        public string Name { get; set; }
        public string Label { get; set; }
    }
}

get Ax UserId from from NetworkAlias in Dynamics Ax

I started by wring something like this

public static UserInfo findUserInfo(NetworkAlias _networkAlias)
{
    UserInfo userInfo;

    select firstOnly userInfo
        where userInfo.networkAlias == _networkAlias;

    return userInfo;
}

this code does the job, but after some research I could find a built in method. I think it is always better to use native code then write your own. It handles many more variables that I didn’t even think of. if you look inside (as you always should) you will see that the above code is quite incomplete.

static void JobUSerInfoFromWindowsUser(Args _args)
{
    UserInfo userInfo;
    AifWindowsUser windowsUser = 'myDomanin.com\\ActiveDirecotryUserName';
    userInfo = AifPortUser::getAxaptaUser(windowsUser);
    info(userInfo.id);
}

Tuesday, August 27, 2019

display method caching in Dynamics Ax

I just got into a problem with a display method which let me dig a bit more on the subject. Let me share with you, maybe is nothing new, but worth remembering.

I created my method on the table like this:

[SysClientCacheDataMethodAttribute(true)]
public display MYD_AnnualContractValue contractValue()
{
    MYD_AnnualContractValue value;

    value = // code to get the value...

    return value;
}
first one consideration (from http://www.axaptapedia.com/CacheAddMethod)

Performance considerations

When caching is enabled for a display or edit method, that result of the method will be calculated on the AOS and passed through to the client with the result set. In general, this is faster than calling the method directly from the client, on demand, as it reduces client-server calls.

This makes good sense when a display method is used as part of a grid, but in other circumstances it is important to gauge whether the cost of having the method calculated every time is worth the reduction in client-server calls. An uncashed display method is triggered only when the control is visible, so it can be better to not use caching for display methods bound to controls on tab pages which are rarely viewed.

Attribute SysClientCacheDataMethodAttribute set to true if you want it updated when a record is saved

Look in \Classes\SysClientCacheDataMethodAttribute\new and see that when you set true the display cache is update on record save. When you set as false it is not. meaning that the display method on the form will not change on update until you close and open the form again

refresh the display when you modify a record (before you save it)

you have two options:

OPTION 1

the simplest option is just to remove the SysClientCacheDataMethodAttribute completely

//[SysClientCacheDataMethodAttribute(true)] //Enrico: do not use cache so the value changes immediately without need to save.
public display MYD_AnnualContractValue contractValue()
{
    //
}

which might make sense especially if this method is not expected to go inside a grid.

OPTION 2

use cacheCalculateMethod as explained in this post: https://smrithisomanna.blogspot.com/2008/12/using-cacheaddmethod.html
which works also if you set the cache on the table rather then using the cacheAddMethod on the datasource.

under the datasource on the field/s that is/are triggering the display method to be updated override the modify method like this:

image

public void modified()
{
    super();
    MyTable_ds.cacheCalculateMethod(tablemethodstr(MyTable, contractValue));
}
in this way you can leave the caching attribute on the table display method so that it is always cached by default and force a refresh programmatically.

Proxy Classes for .NET Interop to X++: session open inside using statement

When you start using Proxy Classes for .NET Interop to X++ you need to open and close a session and in order to adhere to the DRY principle (do not repeat yourself) I created a new c# class to use in my project.

if you look in the examples of how to use Linq to Ax (https://docs.microsoft.com/en-us/dynamicsax-2012/developer/code-example-linq-to-ax-from-csharp) you can see in the code they have to open and close the session at the end:

using       System;  // C#
using       System.Linq;
using U23 = Microsoft.Dynamics.AX.ManagedInterop;
using U22 = Microsoft.Dynamics.AX.Framework.Linq.Data;
using       Microsoft.Dynamics.AX.Framework.Linq.Data; // .ForUpdate() needs this.

namespace LinqProviderSample
{
   class Program  // C#, LINQ to AX.
   {
      static void Main(string[] args)
      {
         // Logon to Dynamics AX.
         U23.Session axSession = new U23.Session();
         axSession.Logon(null, null, null, null);

    //more code...

    axSession.Logoff();
      }
   }
}

but what I want is to use a using statement like this so that I do not have to worry about opening and closing the session and repeat every time the same code:

public string GetTableLabel()
{
    using (AxSessionManager axSession = new AxSessionManager())
    {
        axSession.OpenConnection();

        MyTable record = new MyTable();

        var x = new SysDictTable(record.TableId);

        return x.label();
    }
}

Here the class you need to add to the project:

using System;
using System.Net;
using U23 = Microsoft.Dynamics.AX.ManagedInterop;

namespace MydDynamicsIntegration.DynamicsCommon
{
    public class AxSessionManager : IDisposable
    {
        private readonly U23.Session _axSession = new U23.Session();

        public U23.Session Connection
        {
            get { return _axSession; }
        }

        /// <summary>
        ///     Checks to see if the AX session is connected. If it's null we return false.
        ///     Then we check to see if it's logged in, then return true. Otherwise it's not logged in.
        /// </summary>
        public bool Connected
        {
            get { return _axSession != null && _axSession.isLoggedOn(); }
        }

        public void Dispose()
        {
            CloseConnection();
        }

        /// <summary>
        ///     This connects to the AX session. If it's already connected then we don't need to connect
        ///     again, so we return true. Otherwise we'll try to initiate the session.
        /// </summary>
        /// <returns>
        ///     True: Connection openned successfully, or was already open.
        ///     False: Connection failed.
        /// </returns>
        public bool OpenConnection()
        {
            if (Connected)
            {
                return true;
            }

            try
            {
                _axSession.Logon("fch", "en-GB", null, null);
                return true;
            }
            catch
            {
                return false;
            }
        }

        public bool OpenConnectionAs(NetworkCredential nc, string domain)
        {
            //System.Net.NetworkCredential nc = new System.Net.NetworkCredential("ProxyUserID", "password");
            var strUserName = nc.UserName;

            if (Connected)
            {
                return true;
            }

            try
            {
                //_axSession.LogonAs(strUserName.Trim(), "domain.com", nc, "fch", "en-GB", null, null);
                _axSession.LogonAs(strUserName.Trim(), domain, nc, "fch", "en-GB", null, null);
                return true;
            }
            catch
            {
                return false;
            }
        }

        /// <summary>
        ///     If the session is logged on we will try to close it.
        /// </summary>
        /// <returns>
        ///     True: Connection closed successfully
        ///     False: Problem closing the connection
        /// </returns>
        public bool CloseConnection()
        {
            bool retVal;
            if (Connection.isLoggedOn())
            {
                try
                {
                    _axSession.Logoff();
                    retVal = true;
                }
                catch
                {
                    retVal = false;
                }
            }
            else
            {
                retVal = true;
            }

            Connection.Dispose();
            return retVal;
        }
    }
}


as you see from the code you can either open the connection using
axSession.OpenConnection();
or use OpenConnectionAs where you need to pass the network credentials of the logged in user that match the user in Dynamics to impersonate that user in the session.

validate UK postcode using a free WebService in x++ Dynamics Ax 2012

in Uk there is this free service http://postcodes.io/ which can be used to validate a uk postcode.

here an example on how to use it in x++ using a Web Service and parse the returned Json

static void JobEF_WebServiceTestInDialog(Args _args)
{
    Dialog dialog;
    DialogField zipCodeField;
    LogisticsAddressZipCodeId postcode;
    str result = '"result":';
    str postcodeServiceUri = 'http://api.postcodes.io/postcodes/%1/validate';
    str values, ret;
    int ptr, len;
    System.Net.WebClient webclient;
    str uri;
    NoYesId validPostCode;
    
    webclient = new System.Net.WebClient();
    
    dialog = new Dialog('Validate UK postcode using Web Service');
    
    zipCodeField = dialog.addField(ExtendedTypeStr(LogisticsAddressZipCodeId));
    
    if (dialog.run())
    {
        postcode = zipCodeField.value();
        if (!postcode)
        {
            warning('please select a post code');
            return;
        }
        
        uri = strfmt(postcodeServiceUri, postcode);
        
        // Get the value from the server given the URL.
        values = webclient.DownloadString(uri);

        // Find the result value from the JSON
        len = strLen(values);
        ptr = strScan(values, result, 1, len);
        ret = subStr(values, ptr + strLen(result), len - ptr - strLen(result));
        validPostCode = ret == 'true';
        
        info(strFmt('"%1" is %2', postcode, validPostCode ? 'a valid postcode' : 'an invalid postcode'));
    }
}

Serialize to json using x++ in Dynamics Ax 2012

here an example how to create from x++ a simple json object.

static void JobEF_ListToJson(Args _args)
{
    int i;
    str myJson;
    System.Web.Script.Serialization.JavaScriptSerializer ser = new System.Web.Script.Serialization.JavaScriptSerializer();
    System.Object oVar;
    System.Type listType = System.Type::GetType('System.Collections.ArrayList');
    CLRObject arrayList = System.Activator::CreateInstance(listType);

    new InteropPermission(InteropKind::ClrInterop).assert();
    for( i = 0; i <= 5 ; i++)
    {
        oVar = strFmt('%1 is the value %2', i, DateTimeUtil::utcNow());
        arrayList.Add(oVar);
    }

    myJson = ser.Serialize(arrayList);

    info(myJson);
}

Create a unique random number in Dynamics Ax x++

static void JobEF_RandomNumber(Args _args)
{
    System.Random rand = new System.Random();
    MYTable MYTable;
    int value = rand.Next();
    boolean found;

    info(strFmt('%1', value));
    
    while (!found)
    {
        value = rand.Next(10);
        if ((select firstOnly MYTable where MYTable.ChangeRequestId == value).RecId == 0)
            found = true;
    }
    

    info(strFmt('%1', value));
}

helper class to clear the value of a table field

static boolean clearValueOfField(
    Common _record,
    FieldID _fieldID)
{
    DictField dictField;
    ;

    dictField = new DictField(_record.TableId, _fieldID);
    if (!dictField)
        return false;

    switch (dictField.baseType())
    {
        case Types::String :
        case Types::VarString :
            _record.(_fieldID) = "";
            return true;

        case Types::Date :
            _record.(_fieldID) = datenull();
            return true;

        case Types::Real :
        case Types::Integer :
        case Types::Enum :
            _record.(_fieldID) = 0;
            return true;

        default :
            return false;
    }

    return false;
}

Effective date Form datasource filter Queryrange using check boxes to select expired active future records in Dynamics Ax

I had a requirement to create create a form where the date effective selection appears as check boxes.

image

I found a similar implementation in LogisticsPostalAddress, so here the code I will show you below based is on what you can see in \Forms\LogisticsPostalAddress\Data Sources\LogisticsLocation\Methods\executeQuery

1) override the click event of each checkbox like this:

public void clicked()
{
    super();

    element.setEffectiveDateFilter();
}

2) add this method

here I added some logic to check uncheck based the checkboxes to avoid invalid selections

public void setEffectiveDateFilter()
{
    if (DisplayExpired.value() && !DisplayActive.value() && DisplayFuture.value())
        DisplayActive.value(true);
    if (!DisplayExpired.value() && !DisplayActive.value() && !DisplayFuture.value())
        DisplayActive.value(true);

    MYTable_ds.executeQuery();
}

3) override the executeQuery of the datasource

public void executeQuery()
{
    //based on the template: \Forms\LogisticsPostalAddress\Data Sources\LogisticsLocation\Methods\executeQuery
    QueryBuildRange qbrValidFrom, qbrValidTo;
    RecId curRecordRecId;
    ValidFromDate validFrom = Global::dateNull();
    ValidToDate validTo = Global::dateMax();
    boolean expired;
    boolean active;
    boolean future;
    str queryRangeStr='';

    // Get the record currently selected
    curRecordRecId = MYTable_DS.cursor().RecId;

    expired = DisplayExpired.value();
    active = DisplayActive.value();
    future = DisplayFuture.value();

    qbrValidFrom = SysQuery::findOrCreateRange(this.query().dataSourceNo(1), fieldNum(MYTable,ValidFrom));
    qbrValidTo   = SysQuery::findOrCreateRange(this.query().dataSourceNo(1), fieldNum(MYTable,ValidTo));

    if (expired && !active && !future)
    {
        queryRangeStr = '(%1.%3 <= %4)';
        MYTable_ds.query().validTimeStateDateRange(Global::dateNull(), systemDateGet());
    }
    else if (!expired && active && !future)
    {
        queryRangeStr = DateEffectivenessCheck::queryRange(true,false,false);
        MYTable_ds.query().resetValidTimeStateQueryType();
    }
    else if (!expired && !active && future)
    {
        queryRangeStr = DateEffectivenessCheck::queryRange(false,false,true);
        MYTable_ds.query().validTimeStateDateRange(systemDateGet(), dateMax());
    }
    else
    {
        queryRangeStr = DateEffectivenessCheck::queryRange(true,true,true);
        validFrom = element.calcValidFrom();
        validTo = element.calcValidTo();
        //Debug::printDebug(strFmt('validFrom %1; validTo: %2', validFrom, validTo));
        MYTable_ds.query().validTimeStateDateRange(validFrom, validTo);
    }

    MYTable_ds.validTimeStateUpdate(ValidTimeStateUpdate::Correction);

    if (queryRangeStr)
        {
            qbrValidFrom.value(
                strFmt(queryRangeStr,
                    this.query().dataSourceTable(tableNum(MYTable)).name(),
                    fieldStr(MYTable,ValidFrom),
                    fieldStr(MYTable,ValidTo),
                    DateTimeUtil::toStr(DirUtility::getCurrentDateTime())
                )
            );
        }


    super();

        //focus again on the previously selected value
    MYTable_DS.findValue(fieldname2id(MYTable.TableId, 'RecID'), int642str(curRecordRecId));
}

Tuesday, August 20, 2019

get a list of Dynamics Ax security roles in code x++

static void FCH_SecurityRole(Args _args)
{
    SecurityRole        role;
    SecurityUserRole    userRole;

    while select userRole
    {
        info(userRole.User);

        select firstOnly role where role.RecId == userRole.SecurityRole;

        info(strFmt('%1 - %2', userRole.SecurityRole, SysLabel::labelId2String(role.Name)));

        info('--------------');
    }
}

dynamically create a Query inquiry in x++ code on Dynamics ax

if you try to run this job

static void JobEF_QueryInquiry(Args _args)

{

    QueryBuildDataSource queryBuildDataSource;

    QueryBuildRange queryBuildRange;

    QueryRun queryRun;

    Query    query;

    Qty total;

    InventTrans inventTrans;

    inventDim inventDim;

    query = new Query();

 

    queryBuildDataSource = query.addDataSource(tableNum(InventTrans));

    queryBuildDataSource.addSelectionField(fieldNum(InventTrans,Qty),SelectionField::Sum);

    queryBuildRange      = queryBuildDataSource.addRange(fieldNum(InventTrans,ItemId));

    queryBuildDataSource = queryBuildDataSource.addDataSource(tableNum(InventDim));

    queryBuildDataSource.addGroupByField(fieldNum(InventDim,InventBatchId));

    queryBuildDataSource.relations(true);

    queryRun = new QueryRun(query);

 

    if (queryRun.prompt())

    {

        while (queryRun.next())

        {

            inventTrans = queryRun.get(tableNum(InventTrans));

            inventDim   = queryRun.get(tableNum(inventDim));

            info(strFmt("Batch %1, qty %2",inventDim.inventBatchId, inventTrans.qty));

        }

    }

}


it will prompt you with a query inquiry form that you can fill up with the appropriate values to set the filters


image