Pages

Monday, September 8, 2014

Removing diacritics (accents on letters) from strings in X++

If you want to remove diacritics (accents on letters) from strings like this "ÁÂÃÄÅÇÈÉàáâãäåèéêëìíîïòóôõ£ALEX" to use the more friendly string "AAAAAACEEaaaaaaeeeeiiiioooo£ALEX", you can use this block of code:

static void AlexRemoveDiacritics(Args _args)
{
    str strInput = 'ÁÂÃÄÅÇÈÉàáâãäåèéêëìíîïòóôõ£ALEX';
    System.String input = strInput;
    str retVal;
    int i;

    System.Char c;
    System.Text.NormalizationForm FormD = System.Text.NormalizationForm::FormD;
    str normalizedString = input.Normalize(FormD);
    System.Text.StringBuilder stringBuilder = new System.Text.StringBuilder();

    for (i = 1; i <= strLen(normalizedString); i++)
    {
        c = System.Char::Parse(subStr(normalizedString, i, 1));

        if (System.Globalization.CharUnicodeInfo::GetUnicodeCategory(c) != System.Globalization.UnicodeCategory::NonSpacingMark)
        {
            stringBuilder.Append(c);
        }
    }

    input = stringBuilder.ToString();
    input = input.Normalize();
    retVal = input;

    info(strFmt("Before: '%1'", strInput));
    info(strFmt("After: '%1'", retVal));
}



I did not come up with this code, I merely adapted it from http://www.codeproject.com/Tips/410074/Removing-Diacritics-from-Strings who adapted it from somewhere another person who deleted their blog.  It is still very useful.

Thursday, August 28, 2014

HowTo: Find out how long your AOS Service has been running through PowerShell

If you want to check when you last restarted your AOS service, you can just run the powershell command:

Get-Process Ax32Serv | % {new-TimeSpan -Start $_.StartTime} | select Days,Hours,Minutes,Seconds



Tuesday, August 12, 2014

How to get/set printer settings in AX 2012 for SRS reports

This is a simple job that shows you how to get/set printer settings in AX 2012.  In AX 2009, I used things like PrintJobSettings and SysPrintForm where in AX 2012 you use SRSPrintDestinationSettings which uses SysOperationsTemplateForm.

This is just a "Hello World" if you will for modifying the print settings in AX 2012 and I'll be posting a follow up with a slightly more advanced example.


static void GetPrinterSettingsAX2012Simple(Args _args)
{
    SRSPrintDestinationSettings             printSettings = new SRSPrintDestinationSettings();
    SRSPrintDestinationSettingsContainer    printerNameDestination;
    
    // This sets what the user will see defaulted
    printSettings.printMediumType(SRSPrintMediumType::Printer);
    
    if (SrsReportRunUtil::showSettingsDialog(printSettings))
    {
        printerNameDestination = SrsReportRunUtil::getPrinterNameDestination(printSettings);
        
        if (printerNameDestination != conNull())
        {
            info(strFmt("Printer Name: %1", conPeek(printerNameDestination, 1))); 
            info(strFmt("Printer Destination: %1", conPeek(printerNameDestination, 2)));
        }
    }
    else
        info("User clicked cancel");
}

Friday, August 8, 2014

AX 2012 TFS: Solving update conflicted FK_ModelElementData_HasModelId_LayerId and XU_Update error with SQL profiler

When using AX 2012 with TFS, sometimes you'll get a cryptic error during a TFS synchronization like this:

SQL error description: [Microsoft][SQL Server Native Client 10.0][SQL Server]The UPDATE statement conflicted with the FOREIGN KEY constraint "FK_ModelElementData_HasModelId_LayerId". The conflict occurred in database "Dev5_AX2012_model", table "dbo.Model".
SQL statement: { CALL [Dev5_AX2012_model].[dbo].[XU_Update](?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) }
Cannot execute a stored procedure.




And that's hardly enough information to solve it.

Most likely cause of error for those who don't want to read the entire article:
You probably have a USR layer (or some other layer) that was mistakenly created by any number of things that you should delete.


How to identify/solve:
First, I have to give credit to this post by Fred Shen that got me started down the right path.  He received the same FK_ModelElementData_HasModelId_LayerId error, but with a different root cause.  I couldn't use his steps verbatim though because my TFS sync would take 30+ minutes and that would generate tons of SQL profiler data that I couldn't easily sift though.

  1. Find the error code (547 for me).  SystemAdministration>Inquiries>Database>SQL statement trace log

  2. Launch SQL Profiler and connect to the SQL server
  3. Create new trace, name it whatever you'd like, set a stop time just in case you leave it running, then click on "Events Selection" tab
  4. Uncheck everything
  5. Check "Show all events" and "Show all columns"
  6. Under "Errors and Warnings" check "Exception"
  7. Under "Stored Procedures" check "SP:Starting"
  8. Verify for both of these rows that the columns "TextData", "DatabaseName", and "Error" (where applicable) are checked
  9. Click column filters and on DatabaseName, put your database name or %DBName% for wildcards
  10. Click on the Error filter and put the error code from step 1 (547 in my case)
  11. Lastly, click on TextData and put in "%XU_Update%" and "%FOREIGN KEY%"
  12. Run it, then execute a TFS sync or whatever it is you do to cause the exception to be thrown in AX.
  13. Stop it after you receive the exception and you should see a bright red "Exception" in the profiler.
  14. Press Ctrl+F, and type "exception" and choose the column "EventClass" to search and it'll help you get right to the bad call
Hopefully this helps somebody identify exactly what is throwing their error!


Friday, July 18, 2014

HowTo: Automatically create parm methods on AxBC classes in one line of code!

When you get this best practice error:

"The Table.Field does not have a corresponding parm-method on the AxBC-class"

You can save yourself time and automatically add the methods with this line in a job:

AxGenerateAxBCClass::newTableId(tableNum(WMSPickingRoute)).run();

Now whenever you run code that creates code, you should know what you're doing.  Also, this won't create the set methods, but I think that's a bug on Microsoft's part because part of the code appears to be there, but not used at all.  Look at `\Classes\AxGenerateAxBCClass\createInterfaceMethods` and see the variable `setMethodName` is not used.

I modified that class to make it work for my purposes.

Wednesday, July 16, 2014

Force the batch server to execute batch jobs immediately and bypass the 60 second wait in one line of code! - [AX 2012]

In AX 2012, if you want to force the batch server to immediately check for awaiting batch jobs and execute them without waiting the 60 seconds for the server to discover them, you can call:

xApplication::checkForNewBatchJobs();

Why might you need this?

In my case, I need to call this because I think there is a bug caused by a race condition that can occur with reliable asynchronous (SysOperationExecutionMode::ReliableAsynchronous) processes that are called from the client.

What happens when you call a reliable asynchronous process client side, two things happen at the same time from `\Classes\SysOperationController\doBatch`:

  • Batch task record is inserted to \Data Dictionary\Tables\Batch)
  • Some sort of new thread is spun up asynchronously via `\Classes\SysOperationController\asyncWaitForBatchCompletion`

So, my theory on what can happen is the async polling process (`\Classes\SysOperationFrameworkService\waitForBatchJob`) will try and select the Batch record and may not find it because the Batch record hasn't finished inserting.

In my case, I've overloaded the doBatch() method and put some tracking logic with pessimisticLock that slows down the Batch insert just enough to periodically cause this...so I end up with batch jobs sometimes that are waiting ~60 seconds to be picked up.

Wednesday, July 2, 2014

How to: Copy a sales order using SalesCopying class

There are a million code snippets that show you how to create/copy a sales order that work 80% of the time, but often don't account for the many different scenarios from different environments.

This job shows you how to copy a sales order, but more importantly it shows you how to use the SalesCopying class so that you can copy from SalesQuotes, Journals, etc.


static void JobCopySO(Args _args)
{
    SalesTable  salesTable = SalesTable::find('SO000777');
    SalesLine   salesLine;
    SalesTable  salesTableNew;
    SalesOrderCopyingContract contract = SalesOrderCopyingContract::newIsCreditNote(false);
    
    SalesCopying            salesCopying;
    TmpFrmVirtual           tmpFrmVirtualLines;
    TmpFrmVirtual           tmpFrmVirtualHeader;
    
    void writeTmpFrmVirtual(TmpFrmVirtual _tmpFrmVirtual, TableId _tableId, RecId _recId, Num _id, LineNum _lineNum = 0, TransDate _transDate = systemDateGet(), Qty _qty = 0)
    {
        _tmpFrmVirtual.clear();
        _tmpFrmVirtual.TableNum     = _tableId;
        _tmpFrmVirtual.RecordNo     = _recId;
        _tmpFrmVirtual.Id           = _id;
        _tmpFrmVirtual.LineNum      = _lineNum;
        _tmpFrmVirtual.TransDate    = _transDate;
        _tmpFrmVirtual.Qty          = _qty;

        _tmpFrmVirtual.write();
    }
    
    // Create your new sales header
    salesTableNew.SalesId = NumberSeq::newGetNum(SalesParameters::numRefSalesId()).num();
    salesTableNew.initValue();
    salesTableNew.CustAccount = salesTable.CustAccount;
    salesTableNew.initFromCustTable();
    salesTableNew.insert();
    
    // Build header virtual
    writeTmpFrmVirtual(tmpFrmVirtualHeader, salesTable.TableId, salesTable.RecId, salesTable.SalesId);
    
    while select salesLine
        where salesLine.SalesId == salesTable.SalesId
    {
        writeTmpFrmVirtual(tmpFrmVirtualLines, salesLine.TableId, salesLine.RecId, salesLine.SalesId, salesLine.LineNum, systemDateGet(), salesLine.SalesQty);
    }
    
    contract.parmSalesPurchCopy(SalesPurchCopy::CopyAllHeader);
    contract.parmCallingTableSalesId(salesTableNew.SalesId);
    contract.parmTmpFrmVirtualLines(tmpFrmVirtualLines);
    contract.parmTmpFrmVirtualHeader(tmpFrmVirtualHeader);
    contract.parmQtyFactor(1);
    contract.parmRecalculateAmount(NoYes::No);
    contract.parmReverseSign(NoYes::No);
    contract.parmCopyMarkup(NoYes::No);
    contract.parmCopyPrecisely(NoYes::No);
    contract.parmDeleteLines(NoYes::Yes);    
    
    SalesCopying::copyServer(contract.pack(), false);
       
    info(strFmt("Created %1", salesTableNew.SalesId));
}

Wednesday, June 18, 2014

[AX 2012 Upgrade] Identifying bad EDT relations using reflection and creating custom project with them.

In AX 2012, relations are no longer supported under Extended Data Types.

During an upgrade where you are re-implementing, it's common to import database schema via XPO to get your table structure with EDTs.

Sometimes EDTs that are brought over end up pointing to non-existent tables and/or and you change/rename tables, things get broken.

To uplift your old EDT relations to their corresponding tables, you can use the EDT Relation Migration Tool (http://msdn.microsoft.com/en-us/library/gg989788.aspx) located under Tools>Code Upgrade>EDT Relation Migration Tool.

My first time running this tool, it errored with "The table  does not exist."

Which was due to an EDT that was imported that had a broken table relation.

This job will loop over every CUS+ EDT, and if it has a relation, check if it's valid.  If it is not, it will create a private project of the EDTs with errors.  You can easily change it to search different layers/models/etc.  I only limited to the CUS layer for performance.

static void JobCreateProjWithBrokenEDTs(Args _args)
{
    #define.ProjName('BadEDTs')

    SysProjectFilterRunBase     projectFilter = new SysProjectFilterRunBase();
    ProjectNode                 projectNode;
    UtilElements                utilElements;

    SysModelElement             modelElement;
    SysModelElementType         modelElementType;
    SysModelElementData         modelElementData;

    SysDictType                 sysDictType;
    SysDictRelation             sysDictRelation;
    TableId                     tableId;
    SysDictTable                sysDictTable;
    SysDictField                sysDictField;
    FieldName                   fieldName;
    int                         i;

    void addBadEDT(SysModelElement _modelElement)
    {
        utilElements = null;
        utilElements.Name = _modelElement.Name;
        utilElements.ParentID = _modelElement.ParentId;
        utilElements.RecordType = _modelElement.ElementType;

        if (utilElements.RecordType == UtilElementType::SharedProject ||
            utilElements.RecordType == UtilElementType::PrivateProject ||
            utilElements.RecordType == UtilElementType::ClassInternalHeader ||
            utilElements.RecordType == UtilElementType::TableInternalHeader ||
            !projectFilter.doUtilElements(utilElements))
        {
            info(strfmt("@SYS316339", strfmt('%1 %2', utilElements.RecordType, utilElements.Name)));
        }
    }

    projectFilter.grouping(SysProjectGrouping::AOT);

    while select modelElement
        join Name from modelElementType
            where modelElementType.RecId == modelElement.ElementType    &&
                  modelElementType.RecId == UtilElementType::ExtendedType
        join modelElementData
            where modelElementData.ModelElement == modelElement.RecId   &&
                  modelElementData.Layer        >= (UtilEntryLevel::cus-1)
    {
        sysDictType = new sysDictType(modelElement.AxId);

        if (sysDictType)
        {
            sysDictRelation = sysDictType.relationObject();

            if (sysDictRelation)
            {
                tableId = sysDictRelation.table();

                sysDictTable = new SysDictTable(tableId);

                if (sysDictTable)
                {
                    // Found an EDT with a valid table, check if the field
                    // relations are good
                    for (i = 1; i <= sysDictRelation.lines(); i++)
                    {
                        fieldName = fieldid2name(tableId,sysDictRelation.lineExternTableValue(i));

                        sysDictField = new SysDictField(sysDictTable.id(),fieldname2id(sysDictTable.id(),fieldName));

                        if (!sysDictField)
                        {
                            // Field relation is bad on EDT
                            warning (strFmt("%1 found table, missing field on table %2", sysDictType.name(), sysDictTable.name()));
                            addBadEDT(modelElement);
                        }
                    }

                }
                else
                {
                    // Found an EDT with a broken table relation
                    warning(strFmt("%1 missing valid table", sysDictType.name()));
                    addBadEDT(modelElement);
                }
            }
        }
    }

    SysUpgradeProject::delete(#ProjName, ProjectSharedPrivate::ProjPrivate);
    projectNode = SysTreeNode::createProject(#ProjName);
    projectFilter.parmProjectNode(projectNode);
    projectFilter.write();

    info(strFmt("Created private project %1", #ProjName));
}

Wednesday, June 4, 2014

AX 2012 TFS Synchronizing multiple models at the same time in 1 line of code!

AX TFS version control sync'ing is designed out of the box to allow you to synchronize multiple models, but for some reason Microsoft intentionally disabled this via code.  This is my only concern...why did they intentionally disable it?

I've done some light testing with multiple models and it appears to be working fine synchronizing multiple models.  I've added this code in our development environment with the caveat among the other developers that Microsoft encourages syncing 1 model at a time in code, but syncing multiple seems to work just fine.

Change this one line in \Classes\SysVersionControlUserInterfaceMorphX\promptForFolder to:






And now you can check multiple models to sync at the same time


Wednesday, May 21, 2014

How to use recursion to loop over form controls using object type

This was a stackoverflow answer I provided that neatly displays a way to loop over all form controls using recursion and the object type.  This may be useful when you want to modify/initialize form controls systematically.


Tuesday, May 20, 2014

Reflection on AOT to get delete actions

We are in the middle of an AX 2009 to AX 2012 upgrade, and part of our upgrade steps was to first import our AX 2009 AOT schema only.

During this process we weren't able to bring over the delete actions, so I wrote a quick job to reflect on the AOT that I could use to identify missing custom delete actions.  I think it's a good example of some simple AOT reflection.


Friday, May 9, 2014

HowTo: Update all Cross Reference (XRef) data in batch in 1 line of code in a job! (AX2012)

AX2012: To update your Cross Reference data (XRef) data in your system in BATCH in one line of code, just create this job and run it:

static void updateXRef(Args _args)
{
    xRefUpdateIL::updateAllXref(true, false, true);
    
    info('Updating XRef batch job added!');
}


It will add a batch job and start working.

Note: I'm running AX 2012 R2 CU7.  I'm not sure if this exists in previous AX 2012 versions.

Walkthrough: Detailed steps to creating your own useful AX custom best practice check and custom attribute! Quick & Dirty

I am a big proponent of using tools to systematically perform checks to ensure consistency and quality instead of relying on human intervention, so naturally I am a big fan of writing code to check my code!

I call this quick & dirty because I won't be going deep into some details and will let you investigate on your own.

In this walkthrough I will be showing you both directly and indirectly how to:

  • Create a custom attribute
  • Create a custom Best Practice Check
  • Reflect on the AOT to see if an object has that attribute
  • Throw best practice warnings in the compiler
  • And more!

Node/Disclaimer:

This is proof of concept code which is demonstrating these concepts in the simplest way, but you should use these concepts and refactor them to follow best practices such as using labels, EDTs, etc.  You should also create new methods for commonly used blocks of code.  You can take these concepts and expand them to real-world uses too.

Create the custom attribute:

Create a class and name it "MyCompanyAttribute" and extend SysAttribute.  Add a member variable "int feature;" to the classDeclaration.

class MyCompanyAttribute extends SysAttribute
{
    int feature;
}


Add an accessor method to get the feature value.

public int feature()
{
    return feature;
}

Important!

Create the new() method. This method is important to assign the variables, and XML documentation is crucial here too for intellisense to display to developers.  Note that I default "feature = 0" in the arguments so that it will make this parameter optional.

Intellisense will look like this:



/// <summary>
/// Initializes an instance of the <c>MyCompanyAttribute</c> class
/// </summary>
/// <param name="_feature">
/// Internal company feature number for customization tracking
/// </param>
/// <remarks>
/// This attribute should be used on custom methods added to base classes
/// </remarks>
public void new(int _feature = 0)
{
    super();

    feature = _feature;
}

Custom attribute done!

Add Best Practice Check and hook:


The class `\Classes\SysBPCheckMemberFunction` is what checks all member functions.  I will be limiting the code to strictly only checking class member functions in the CUS+ layer.

First create your new method that does the work:

protected boolean checkMyCompanyAttribute()
{
    boolean         ret = true;
    UtilElements    utilElements;

    switch (SysDictMethod.utilElementType())
    {
        case UtilElementType::ClassInstanceMethod:
        case UtilElementType::ClassStaticMethod:
            // Find the lowest layer this object exists in
            select firstOnly RecId, UtilLevel from utilElements
                order by UtilLevel asc
                where utilElements.parentId     == SysDictMethod.parentId()
                   && utilElements.recordType   == sysDictMethod.utilElementType()
                   && utilElements.name         == sysDictMethod.name();

            // If the lowest layer is CUS+ then we want our custom checks enforced
            if (utilElements.utilLevel >= UtilEntryLevel::cus)
            {
                // Now check if the attribute is not decorating the method
                if (SysDictMethod.getAttribute(attributeStr(MyCompanyAttribute)) == null)
                {
                    // Developer has failed the BP check!  Let them know
                    ret = false;

                    // This is what puts the BP error in the compiler window.
                    SysBPCheck.addError(0, 1, 1, 'MyCompanyAttribute: Missing custom [MyCompanyAttribute] on custom CUS+ layer method.');
                }
            }

            break;

        default:
            ret = true;
    }

    return ret;
}


Then in `\Classes\SysBPCheckMemberFunction\check` this is where you put your hook to call your BP check.



MS Best Practice Note: If you were to do this for a customer or in a live system, you should really create an EDT(s) and parameter(s) on `\Data Dictionary\Tables\SysBPParameters` then add them to `\Forms\SysBPSetup` so that users can enable/disable them.

Final Steps to get it picked up and active:

Turn on Best Practice Checks if you haven't already! See http://msdn.microsoft.com/en-us/library/cc967377(v=ax.50).aspx for detailed directions or the abbreviated directions are just set your compiler to "Level 4"

You may need to recompile `SysBPCheckMemberFunction` or better yet just compile forward `\Classes\SysBPCheckBase`.

You may need to close/reopen your compiler and/or client too.  When the compiler window is opened, it builds various maps.

Results:

It works!  It should error on your new custom method.

You will notice a purple underline on the method too:

Let's add our attribute and recompile the class (not just the method) and resolve the BP check:

Closing Notes:

In my code, I used error code 0, which doesn't appear to be used by Microsoft.  You should really define a new macro definition and error code in `\Macros\SysBPCheck`.

The error code appears to be used for two things.

  1. Obvious: Provide an easy error code # to look-up instead of searching the error string, especially if the string is truly something like `Error on %1` where the string changes depending on the code.
  2. Not so obvious: `\Classes\SysBPCheck` builds an ignoreMap(ErrorCode[int], Paths[Set of strings]) and the error code is the key.
I recommend prefixing your error message with something (like "Alex:" )to clearly identify your BP check as custom, especially if your BP check has a bug or scenario you hadn't anticipated and is mistakenly being thrown.

A comment on reusing the same error code.  If you throw the same BP error on the same method/line/column, it will only take the last one.  The same error can't happen twice on the same method/line/column logically so this makes sense.  So because of this, if you use (0,1,1) for the first three parameters and just change the error message, sometimes when you intend to display multiple messages, only one will appear.

Ignore rules: 

If you have some specific objects you always want excluded, you can add them to `\Macros\SysBPCheckIgnore` and make sure your error code is added in `\Macros\SysBPCheck`.

Alternatively, you can just create a custom Set of paths then union them to the ignoreMap created in `\Classes\SysBPCheck\initIgnoreMap`.

Monday, May 5, 2014

Difference between "modified" and "dataChanged" form data methods on datasource fields

When overriding methods on a form data object, it is important to know what they do exactly!  Two methods in particular, formdataobject.modified() and formdataobject.dataChanged(), sound very similar but are different.


The important difference is that dataChanged() runs EVERY time the data is changed, while modified() only runs after successful validation!

This may seem like a "duh" post for some, but I was surprised at the number of people who couldn't tell me the difference.

So if a user changes data on a form, it may fail validation and modified() would never run.  This is often where you would prefer to hook your code since if the field doesn't truly change, you don't want anything triggered.

Tuesday, April 1, 2014

Models: How to enumerate every object and full path in a given Model [AX 2012]

I have some custom Best Practice checks that have required me to crawl over various specific models and paths and I needed a way to enumerate the exact paths of everything in a model, so I wrote some code to do so!

NOTE: This code won't show some objects like Jobs or Shared/Private Projects. It really shows everything you'd want. This is only because the last join to parentModelElement doesn't display records where modelElement.ParentModelElement == 0.

Just comment out the last block to get some of those odd-objects.

        /*
        join Name from parentModelElement
            where parentModelElement.RecId == modelElement.ParentModelElement
        */


static void listAllModelObjects(Args _args)
{
    SysModelElement     modelElement, parentModelElement;
    SysModelElementType modelElementType;
    SysModelElementData modelElementData;
    SysModelManifest    modelManifest;
    
    while select Name from modelElement
        join Name from modelElementType
            where modelElementType.RecId == modelElement.ElementType
        join modelElementData
            where modelElementData.ModelElement == modelElement.RecId
        join modelManifest
            where modelManifest.Name == 'USR Model'
                && modelManifest.Model == modelElementData.ModelId
        join Name from parentModelElement
            where parentModelElement.RecId == modelElement.ParentModelElement
    {
        info(strFmt("%1, %2, %3, %4",
            parentModelElement.Name, modelElementType.Name, modelElement.Name,
            SysTreeNode::modelElement2Path(modelElement)));
    }
                
}

Monday, March 31, 2014

Creating custom keyboard shortcuts

I'm working on my own custom Editor Scripts and I wanted some keyboard shortcuts for the more frequently used scripts and I came across this fantastic blog post on how to do it in AX.

Keep checking back, as I'll make my own post if/when I can get a working demo up.

See it here: http://www.agermark.com/2011/04/create-your-own-shortcuts-in-ax-forms.html

Models: Notes about models and how to find what model your object is in via X++ [AX 2012]

I'm creating custom best practice checks, and I needed to determine what model an object was in from X++. This builds off of my previous post about enumerating models http://alexondax.blogspot.com/2014/03/models-how-to-enumerate-list-of-models.html.

Important notes/conclusions about models:

  • Models are layer-specific
    • Therefore a parent object (eg \Classes\SalesFormLetter\construct) may exist in the [Foundation] model in the SYS layer, but may also exist in the [USR Model] in the USR layer
  • You can have unlimited models in any layer
  • Models can contain objects that may be parents or children
    • This code below tells you what model C\SalesFormLetter is in, in the SYS layer.  If you have your own custom method in the CUS layer on this object, you will need to use SysDictMethod for example to identify it.
    • You would need to reflect on the objects entirely to fully identify an object.  This is proof of concept code
Models are very simple once you understand their concepts.  They can appear daunting at first.  You can use the TreeNode object to get the model.


static void jobGetObjectModel(Args _args)
{
    SysDictClass        sysDictClass = new SysDictClass(classNum(SalesFormLetter));
    SysModel            sysModel;
    SysModelManifest    sysModelManifest;
    
    
    select Model, Name, Publisher, DisplayName from sysModelManifest
        where sysModelManifest.Model == sysDictClass.treeNode().AOTGetModel()
        join firstonly Layer from sysModel
        where sysModel.RecId == sysModelManifest.Model &&
              sysModel.Layer == UtilEntryLevel::sys; // Change layer here
    
    if (sysModelManifest)
        info(strFmt("Model: %1", sysModelManifest.Name));
    else
        info("Object is not apart of model in given layer");

}

Models: How to enumerate list of models from X++ [AX 2012]

I'm creating custom Best Practice checks and I needed to enumerate the existing models in my model store. Here is a simple job that demonstrates how.  My next post will build on this.  The code is a tweak of \Classes\SysModelStore\buildSelectionModels.


static void jobEnumerateModels(Args _args)
{
    SysModel sysModel;
    SysModelManifest sysModelManifest;
    container selectionList;

    // Select models from specified layer
    while select Model, Name, Publisher, DisplayName from sysModelManifest
        join firstonly Layer from sysModel
        where sysModel.RecId == sysModelManifest.Model &&
              sysModel.Layer == UtilEntryLevel::usr
    {
        selectionList += [SysListSelect::packChoice(strFmt('DisplayName: "%1"\n ModelName: "%2"\n Publisher: "%3"', sysModelManifest.DisplayName, sysModelManifest.Name, sysModelManifest.Publisher), any2int(sysModelManifest.Model), false)];
    }
    conView(selectionList);
}

Monday, February 3, 2014

Incredibly simple and useful tool for formatting SQL code

I'm often debugging SQL queries dumped out of AX by running:

info(query.dataSourceNo(1).toString());

And I get an ugly, long unformatted SQL, where I'm left pressing enter and tab a bunch to make the string somewhat readable.


After some light googleing, I came across an open source SQL Server Management Studio (SSMS) plugin called PoorSQL.  It can also be done online, hooked to Notepad++, etc, but I prefer the plugin.

After pressing Ctrl+K,F, voila!  So useful!


Download link and web link.  Not sure why they have different main URLs:

  • Download - http://architectshack.com/PoorMansTSqlFormatter.ashx#Download_5
  • Web URL - http://poorsql.com/

Wednesday, January 15, 2014

Silly/Rare TFS error with Dynamics AX "Team Server connection error. [Microsoft][ODBC SQL Server Driver]Cannot generate SSPI context"



Team Server connection error. [Microsoft][ODBC SQL Server Driver]Cannot generate SSPI context

This error, when using TFS, means that your AOS user can not authenticate. Google didn't help me much here right away, and hopefully this saves somebody some head-ache.

In my development system, I run the AOS as my user account, and my password expired over the weekend and I changed it.  And an expired password won't cause your AOS to suddenly crash either.

I just went to the AOS service, re-entered my user/password information and voila.