Pages

Wednesday, June 3, 2015

How to export/import your MorphX VCS settings and history

When using MorphX for version control, sometimes you need to restore a backup of a database, and you don't want to lose all of your check in/out history.  I wrote this job to export and import your MorphX VCS settings.

Use at your own risk, but it's worked fine for me.

Enjoy!


static void AKBackupMorphXVCData(Args _args)
{
    SysDataExport           sysDataExport;
    SysDataImport           sysDataImport;
    
    Dialog                  dialog = new Dialog();
    FormBuildRadioControl   fbImportExport;
    FormRadioControl        radioResults;
    
    dialog.addText("Warning, if you choose Import, this will replace your VCS data and is not reversible!");
    
    // Add the radio button, name it anything
    fbImportExport = dialog.formBuildDesign().addControl(FormControlType::RadioButton, 'RadioButton1');
    fbImportExport.caption("Choose Import/Export");
    fbImportExport.items(2); 

    fbImportExport.item(1);
    fbImportExport.text("Export");
    
    fbImportExport.item(2);
    fbImportExport.text("Import");

    dialog.doInit();
    dialog.formRun().design().moveControl(fbImportExport.id());
    dialog.run();

    if (dialog.closedOk())
    {
        radioResults = dialog.formRun().control(fbImportExport.id());
        
        if (radioResults.selection() == 0) // Export
        {
                sysDataExport = new SysDataExport();
                sysDataExport.parmDoNotBypassDefIO(true);
                sysDataExport.parmServerAccess(true);
                sysDataExport.addTmpExpImpTable(tableNum(SysVersionControlMorphXItemTable), false);
                sysDataExport.addTmpExpImpTable(tableNum(SysVersionControlMorphXLockTable), false);
                sysDataExport.addTmpExpImpTable(tableNum(SysVersionControlMorphXRevisionTable), false);
                sysDataExport.addTmpExpImpTable(tableNum(SysVersionControlParameters), false);
                sysDataExport.addTmpExpImpTable(tableNum(SysVersionControlSynchronizeLog), false);

                if (sysDataExport.prompt())
                {   
                    sysDataExport.parmFiletype(FileType::Binary);
                    sysDataExport.run();
                }
        }
        else if (radioResults.selection() == 1) // Import
        {
            sysDataImport = new SysDataImport();

            if (sysDataImport.prompt())
            {
                sysDataImport.parmLoadAll(true);
                sysDataImport.parmInclTablesNotPerComp(true);
                sysDataImport.parmFiletype(FileType::Binary);
                sysDataImport.run();

                versioncontrol.init();
            }
        }
        
        info("Done!");
    }
}

Dynamic dialog controls at runtime

This is a job that demonstrates how to dynamically add controls (specifically radio button) to a dialog at runtime and also change their position and access their values.

You can use this style in custom advanced UI builders.

static void AKDynamicDialogExample(Args _args)
{
    Dialog                  dialog = new Dialog();
    FormBuildRadioControl   fbRadioControl;
    FormRadioControl        radioControl;
    
    // Add the radio button, name it anything
    fbRadioControl = dialog.formBuildDesign().addControl(FormControlType::RadioButton, 'RadioButton1');

    // Set radio basic properties
    fbRadioControl.caption("Test Radio Buttons");
    fbRadioControl.items(2); // This is needed

    fbRadioControl.item(1); // Switch to first item
    fbRadioControl.text("Item 1"); // Set first item's text
    
    fbRadioControl.item(2); // Switch to second item
    fbRadioControl.text("Item 2"); // Set second item's text

    // This is needed to instantiate the FormRun
    dialog.doInit();

    // Just passing one argument moves it UP.
    // So this moves it UP above the "OK/Cancel" buttons created
    dialog.formRun().design().moveControl(fbRadioControl.id());

    dialog.run();

    if (dialog.closedOk())
    {
        // You need to access it from the formRun() with the correct
        // form control
        radioControl = dialog.formRun().control(fbRadioControl.id());
        info(strFmt("%1", radioControl.selection()));
    }
}



Monday, May 11, 2015

How to create/update a phone number for a customer/vendor in X++ [AX 2012]

As a follow-up to my last post about finding phone numbers, here is sample code of how you can properly find/create/update a phone number for a customer/vendor.


static void CreatePhoneExample(Args _args)
{
    CustTable                           custTable = CustTable::find('100013'); // TODO - Change to your customer
    LogisticsElectronicAddress          logisticsElectronicAddress;
    container                           defaultRole = map2Con(LogisticsLocationEntity::getDefaultLocationRoleFromEntity(tableNum(DirPartyTable)));
    
    setPrefix(strFmt("Creating/Updating number for customer %1", custTable.AccountNum));
    
    // This will find/create a number for a customer
    ttsBegin;
    logisticsElectronicAddress.Type = LogisticsElectronicAddressMethodType::Phone;
    logisticsElectronicAddress.Locator = '555-555-5555';
    
    logisticsElectronicAddress.Location = DirPartyLocation::findOrCreate(custTable.Party, 0).Location;

    // This will find or create the new logisticsElectronicAddress
    // If it does not find it, it will do a .insert() which will only persist these fields (Location, Type, Locator, LocatorExtension)
    // so if you want to set the Description or if it's primary or not, you will need to update the record after this call
    logisticsElectronicAddress = LogisticsElectronicAddress::findOrCreate(logisticsElectronicAddress);
    
    // We re-select it for update in case this isn't a new number and it found an existing
    // because the "find" doesn't "select for update"
    logisticsElectronicAddress = LogisticsElectronicAddress::findRecId(logisticsElectronicAddress.RecId, true);
    
    logisticsElectronicAddress.Description = "New Primary Phone";
    
    // If you set the number to primary, during the insert/update it will handle unassigning previously
    // marked primary numbers if they exist
    logisticsElectronicAddress.IsPrimary = NoYes::Yes;
    
    logisticsElectronicAddress.update();
    
    // At this point, we need to mark the "purpose" of the number.  I'm just using the default role, which should be "Business"
    LogisticsEntityLocationRoleMap::createEntityLocationRoles(tableNum(LogisticsElectronicAddressRole), logisticsElectronicAddress.RecId, conPeek(defaultRole, 1), true);
    
    info(strFmt("Created/updated phone number [%1] for customer %2.", logisticsElectronicAddress.Locator, custTable.AccountNum));
    ttsCommit;
}

How the phone numbers relate to customers/vendors and how to search/enumerate them [AX 2012]

The customer/vendor phone number relation is not terribly complex, but there don't seem to be very many clear-cut examples of sample code on how the joins work.  So I typed up a few sample scenarios.

How to find all of a customer/vendor phone numbers:


    // Find all of the customer's phone numbers
    while select dirPartyLocation
        where dirPartyLocation.Party            == dirPartyTable.RecId      &&
              dirPartyLocation.IsPostalAddress  == NoYes::No
    join logisticsElectronicAddress
        order by logisticsElectronicAddress.IsPrimary desc    
        where logisticsElectronicAddress.Location == dirPartyLocation.Location
    {
        info(strFmt("All Numbers\t%1 [Primary == %2]", logisticsElectronicAddress.Locator, logisticsElectronicAddress.IsPrimary));
    }

How to find all of a customer/vendor phone numbers with the "Business" role type:

    // Find all of a customer's phone numbers with the purpose of "Business"
    while select dirPartyLocation
        where dirPartyLocation.Party                            == dirPartyTable.RecId      &&
              dirPartyLocation.IsPostalAddress                  == NoYes::No
    join logisticsElectronicAddress
        order by logisticsElectronicAddress.IsPrimary desc
        where logisticsElectronicAddress.Location               == dirPartyLocation.Location
    join logisticsElectronicAddressRole
        where logisticsElectronicAddressRole.ElectronicAddress  == logisticsElectronicAddress.RecId
    join locationRole
        where logisticsElectronicAddressRole.LocationRole       == locationRole.RecId       &&
              locationRole.Type                                 == LogisticsLocationRoleType::Business
    {
        info(strFmt("All Business Numbers Only\t%1 [Primary == %2, Role == %3]", logisticsElectronicAddress.Locator, logisticsElectronicAddress.IsPrimary, locationRole.Name));
    }

How to find all of a customer/vendor phone numbers and all marked roles:

    // Find all of a customer's phone numbers and all of the marked purposes for each number
    while select dirPartyLocation
        where dirPartyLocation.Party                    == dirPartyTable.RecId      &&
              dirPartyLocation.IsPostalAddress          == NoYes::No
    join logisticsElectronicAddress
        order by logisticsElectronicAddress.IsPrimary desc
        where logisticsElectronicAddress.Location                == dirPartyLocation.Location
    {
        purposes = conNull();
        
        while select logisticsElectronicAddressRole
            where logisticsElectronicAddressRole.ElectronicAddress   == logisticsElectronicAddress.RecId
        join locationRole
            where logisticsElectronicAddressRole.LocationRole        == locationRole.RecId
     
        {       
            purposes += locationRole.Name;
        }
        
        info(strFmt("All Numbers and all roles\t%1 [Primary == %2, Roles == %3]", logisticsElectronicAddress.Locator, logisticsElectronicAddress.IsPrimary, con2Str(purposes)));
    }

And if you are only looking for a primary contact, that information/link is also directly stored on DirPartyTable for the primaries only and you can query it this way:

    // Alternative method for finding the primary locator contact
    // Can see used here \Classes\DirParty\primaryElectronicAddress
    select firstonly logisticsElectronicAddress
        exists join dirPartyTable
            where dirPartyTable.PrimaryContactPhone == logisticsElectronicAddress.RecId
            && dirPartyTable.RecId == custTable.Party;
    
    info(strFmt("Alternative method\t%1 [Primary == %2]", logisticsElectronicAddress.Locator, logisticsElectronicAddress.IsPrimary));

Here are all of the snippets in one large job you can copy & paste and run (after changing to your own customer)

static void FindPhoneExample(Args _args)
{
    CustTable                           custTable = CustTable::find('100013'); // TODO - Change to one of your customers
    DirPartyTable                       dirPartyTable = DirPartyTable::findRec(custTable.Party);
    DirPartyLocation                    dirPartyLocation;
    LogisticsElectronicAddress          logisticsElectronicAddress;
    LogisticsElectronicAddressRole      logisticsElectronicAddressRole;
    LogisticsLocationRole               locationRole;
    container                           purposes;

    
    info(strFmt("%1", custTable.phone()));
    setPrefix("Finding Phone Numbers");
    
    // Find all of the customer's phone numbers
    while select dirPartyLocation
        where dirPartyLocation.Party            == dirPartyTable.RecId      &&
              dirPartyLocation.IsPostalAddress  == NoYes::No
    join logisticsElectronicAddress
        order by logisticsElectronicAddress.IsPrimary desc    
        where logisticsElectronicAddress.Location == dirPartyLocation.Location
    {
        info(strFmt("All Numbers\t%1 [Primary == %2]", logisticsElectronicAddress.Locator, logisticsElectronicAddress.IsPrimary));
    }
    
    // Find all of a customer's phone numbers with the purpose of "Business"
    while select dirPartyLocation
        where dirPartyLocation.Party                            == dirPartyTable.RecId      &&
              dirPartyLocation.IsPostalAddress                  == NoYes::No
    join logisticsElectronicAddress
        order by logisticsElectronicAddress.IsPrimary desc
        where logisticsElectronicAddress.Location               == dirPartyLocation.Location
    join logisticsElectronicAddressRole
        where logisticsElectronicAddressRole.ElectronicAddress  == logisticsElectronicAddress.RecId
    join locationRole
        where logisticsElectronicAddressRole.LocationRole       == locationRole.RecId       &&
              locationRole.Type                                 == LogisticsLocationRoleType::Business
    {
        info(strFmt("All Business Numbers Only\t%1 [Primary == %2, Role == %3]", logisticsElectronicAddress.Locator, logisticsElectronicAddress.IsPrimary, locationRole.Name));
    }
    
    // Find all of a customer's phone numbers and all of the marked purposes for each number
    while select dirPartyLocation
        where dirPartyLocation.Party                    == dirPartyTable.RecId      &&
              dirPartyLocation.IsPostalAddress          == NoYes::No
    join logisticsElectronicAddress
        order by logisticsElectronicAddress.IsPrimary desc
        where logisticsElectronicAddress.Location                == dirPartyLocation.Location
    {
        purposes = conNull();
        
        while select logisticsElectronicAddressRole
            where logisticsElectronicAddressRole.ElectronicAddress   == logisticsElectronicAddress.RecId
        join locationRole
            where logisticsElectronicAddressRole.LocationRole        == locationRole.RecId
     
        {       
            purposes += locationRole.Name;
        }
        
        info(strFmt("All Numbers and all roles\t%1 [Primary == %2, Roles == %3]", logisticsElectronicAddress.Locator, logisticsElectronicAddress.IsPrimary, con2Str(purposes)));
    }
    
    // Alternative method for finding the primary locator contact
    // Can see used here \Classes\DirParty\primaryElectronicAddress
    select firstonly logisticsElectronicAddress
        exists join dirPartyTable
            where dirPartyTable.PrimaryContactPhone == logisticsElectronicAddress.RecId
            && dirPartyTable.RecId == custTable.Party;
    
    info(strFmt("Alternative method\t%1 [Primary == %2]", logisticsElectronicAddress.Locator, logisticsElectronicAddress.IsPrimary));
    
    info("Done");
}

Wednesday, February 25, 2015

Are you sure that you want to cancel this operation? Keeps popping up fix


If "Are you sure that you want to cancel this operation?" keeps popping up over and over when you open AX, the fix is, when the prompt is up, press Ctrl+Pause, then click "No".

This happens to me all the time because Ctrl+Alt+Pause is a shortcut to make a remote desktop window full screen...and if AX is open and catches some of the keystrokes, it puts it in some sort of weird loop.

Monday, February 2, 2015

How to refresh AX WSDL configuration from a command prompt

My build/release process is almost entirely automated, except for one step, where we refresh the WSDL/WCF configuration.  So far, I've only been able to do it conventionally with the mouse.


After getting pointed in the right direction by Martin DrĂ¡b, I wrote a little command line tool in C# that you can incorporate into your build scripts that should refresh your WCF configuration in your AXC file automatically.

It has one dependency on the AX client configuration tool obviously, which is located at:
C:\Program Files\Microsoft Dynamics AX\60\BusinessConnector\Bin\AxCliCfg.exe

Usage: RefreshAXCConfig.exe <axc file> <aos name> <WSDL Port>
Example: RefreshAXCConfig.exe C:\CUS.axc DevAOS 8101

The only caveat that I'm now realizing as of typing this up, is it uses RegEx to find/replace in the AXC file, so it requires you to have refreshed your WCF at least once, otherwise the RegEx won't find what to replace.

The C# code is simple below, and make sure to add the reference to AxCliCfg.exe.  Happy DAX'ing and hopefully this helps someone.  I've also saved the ZIP'd executable to my OneDrive.  You will need to copy the AxCliCfg to the same local directory in order for it to work.

Link to compiled zipped executable for those who don't feel like typing up themselves.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.IO;
using System.Xml;
using Microsoft.Dynamics.Configuration;

namespace RefreshAXCConfig
{
    class Program
    {
        static void Main(string[] args)
        {
            Tuple<Guid, string> result;
            string axcFile;
            string strAOS;
            int intWSDLPort;
            
            if (args.Length == 0)
            {
                Console.WriteLine("Usage: RefreshAXCConfig.exe <axc file> <aos name> <WSDL Port>");
                Console.WriteLine("Example: RefreshAXCConfig.exe CUS.axc Dev-vwaos05 8101");
                return;
            }

            try
            {
                axcFile = args[0];
                strAOS = args[1];
                if (int.TryParse(args[2], out intWSDLPort) == true)
                {
                    result = FormRegenerateWcfDialog.GetConfigurationAsString(strAOS, intWSDLPort);

                    XmlDocument xmlDoc = new XmlDocument();
                    xmlDoc.LoadXml(result.Item2);

                    File.WriteAllText(axcFile, Regex.Replace(File.ReadAllText(axcFile), @"<\?xml.*\</configuration\>", xmlDoc.InnerXml));
                }
            }
            catch (Exception e)
            {
                Console.WriteLine("Error encountered: {0}", e.Message);
            }

            return;
        }
    }
}

Thursday, January 15, 2015

How to extend TFS and create a bug/workitem from AX 2012 X++!

This is a sample job that shows how to create a bug in TFS from AX by extending Team Foundation Server using the available TFS assemblies.

You need to add two references to the TFS assemblies.

First copy these files to your client bin direcotry (C:\Program Files (x86)\Microsoft Dynamics AX\60\Client\Bin):

  • C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE\ReferenceAssemblies\v2.0\Microsoft.TeamFoundation.WorkItemTracking.Client.dll
  • C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE\ReferenceAssemblies\v2.0\Microsoft.TeamFoundation.Client.dll
Then in the AOT, on the References node, right click and click "Add Reference".  Then choose "Browse" and navigate to these two files.


Then if you have Team Foundation Server setup as your Version Control System in AX, you can just run this job and it'll create a test bug!  If you have TFS somewhere else, you can just adjust the job some.

I also showed how to enumerate allowed fields, validate before saving the bug, etc.  I have a more complicated class that I wrote that does more error handling, but I tried to keep this as simple as possible for demo purposes.

I currently haven't quite figured out how I want to use this yet.  I was thinking of creating a new MenuItemButton on the infolog form that a select set of users would have security to.  And if an error was present in the infolog, the user could click "Submit Bug".  Still thinking of ideas.

Please also note the couple comments as they will explain a little more.

Good luck, happy New Year, and happy DAXing!


static void JobCreateTFSBug(Args _args)
{
    Microsoft.TeamFoundation.Client.TfsTeamProjectCollection tfs;
    Microsoft.TeamFoundation.WorkItemTracking.Client.WorkItemStore store;
    Microsoft.TeamFoundation.WorkItemTracking.Client.WorkItemTypeCollection witc;
    Microsoft.TeamFoundation.WorkItemTracking.Client.WorkItemType wit;
    Microsoft.TeamFoundation.WorkItemTracking.Client.Project project;
    Microsoft.TeamFoundation.WorkItemTracking.Client.ProjectCollection projectCollection;
    Microsoft.TeamFoundation.WorkItemTracking.Client.WorkItem workItem;
    SysVersionControlParameters parameters = SysVersionControlParameters::find();

    System.Boolean  netBool;
    System.Collections.ArrayList    invalidFields;
    System.Int32    netInt;
    Microsoft.TeamFoundation.WorkItemTracking.Client.Field      field;
    Microsoft.TeamFoundation.WorkItemTracking.Client.AllowedValuesCollection    allowedValues;
    System.String   netStr;
    str             s, s2;
    int             retVal;
    int             i, n;
    int             arrayCount, arrayCount2;


    try
    {
        tfs    = Microsoft.TeamFoundation.Client.TfsTeamProjectCollectionFactory::GetTeamProjectCollection(Microsoft.TeamFoundation.Client.TfsTeamProjectCollection::GetFullyQualifiedUriForName(parameters.TfsServer));
        store = new Microsoft.TeamFoundation.WorkItemTracking.Client.WorkItemStore(tfs);
        projectCollection   = store.get_Projects();
        project             = projectCollection.get_Item(parameters.TfsProject);
        witc                = project.get_WorkItemTypes();
        wit                 = witc.get_Item('Bug');

        workItem = wit.NewWorkItem();

        workItem.set_Title("Bug Title");
        workItem.set_Description("Bug description");

        // This is how you set a custom field
        //workItem.set_Item('CustomField', 'CustomData');

        // These two lines are equivalent
        // workItem.set_Item('Assigned To', 'AlexOnDAX'); // This is the same as the line below
        workItem.set_Item(Microsoft.TeamFoundation.WorkItemTracking.Client.CoreField::AssignedTo, 'AlexOnDAX');

        netBool = workItem.IsValid();
        if (netBool.Equals(false))
        {
            setPrefix("Error creating work item");
            invalidFields = workItem.Validate();

            netInt = invalidFields.get_Count();
            arrayCount = netInt;
            for (i=0; i<arrayCount; i++)
            {
                field = invalidFields.get_Item(i);
                s = field.get_Name();
                s2 = field.get_Value();
                error(strFmt("Error creating work item\tField '%1' with value '%2' is invalid", s, s2));

                allowedValues = field.get_AllowedValues();
                netInt = allowedValues.get_Count();
                arrayCount2 = netInt;

                for (n=0; n<arrayCount2; n++)
                {
                    netStr = allowedValues.get_Item(n);
                    s = netStr;

                    warning(strFmt("Error creating work item\tAllowed values\t %1", s));
                }
            }
            throw Exception::Error;
        }
        else
        {
            workItem.Save();

            netInt = workItem.get_Id();
            retVal = netInt;
        }
    }
    catch
    {
        error(strFmt("@SYS343139", parameters.TfsServer));
    }

    info(strFmt("Created bug %1", retVal));
}