- [HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\AOS60$01]
- Windows service configurations
- HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Dynamics Server
- AOS Server(s) configurations
- HKEY_CURRENT_USER\Software\Microsoft\Dynamics\
- When a user opens the client config, this registry key is created and it stores their configurations which take priority over HKLM
- HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Dynamics
- These are the client config settings for the machine that are used when HKCU settings aren't present.
This technical blog will be about my adventures with Microsoft Dynamics 365 for Operations (AX7/D3fo), AX 2012, and AX 2009.
Thursday, December 3, 2015
Some notes on Dynamics AX 2012 and the registry
Tuesday, November 10, 2015
CacheDataMethod (AX2012 feature) property vs CacheAddMethod
A someone little known feature of AX 2012 is a new property called "CacheDataMethod".
Previously, when you wanted to improve performance by caching your display/edit methods, you would place a line of code like this after the super() in the datasource's init() method:
In AX 2012, you can just change the "CacheDataMethod" property on the object without adding code. I prefer this method where possible mainly because it keeps everything packaged together and I don't have to intermingle my code with base code.
Previously, when you wanted to improve performance by caching your display/edit methods, you would place a line of code like this after the super() in the datasource's init() method:
this.cacheAddMethod(tableMethodStr(CustTable, MyDisplayMethod));
In AX 2012, you can just change the "CacheDataMethod" property on the object without adding code. I prefer this method where possible mainly because it keeps everything packaged together and I don't have to intermingle my code with base code.
Monday, November 9, 2015
The better way to pass containers between objects/forms using the Args() class, and not by converting to a string
If you need to pass a container between objects/forms using the Args class, don't convert it to a string and then back, use parmObject() and ContainerClass()! I see many suggestions about converting it to a string, which can have much more unpredictable results and is not as versatile.
You wrap your container in the class ContainerClass() and then unwrap it at the other end.
Make your call like this:
To test this, create a form (Form1) and overwrite the init method and put in this code:
Then create a Job and put in this code:
And then run the job!
You wrap your container in the class ContainerClass() and then unwrap it at the other end.
Make your call like this:
And retrieve it like this:args.parmObject(new ContainerClass(["Real container", 1234, "Not con2str container"]));
containerClass = element.args().parmObject() as ContainerClass;
myContainer = containerClass.value();
To test this, create a form (Form1) and overwrite the init method and put in this code:
public void init() { ContainerClass containerClass; container conValue; if (!(element.args() && element.args().parmObject() && element.args().parmObject() is ContainerClass)) throw error("@SYS22539"); super(); containerClass = element.args().parmObject() as ContainerClass; conValue = containerClass.value(); info(strFmt("The container contains '%1'", con2Str(conValue))); }
Then create a Job and put in this code:
static void JobForm1(Args _args) { Args args; FormRun formRun; args = new Args(); args.name(formStr(Form1)); args.parmObject(new ContainerClass(['Real containers', 1234, 'Not con2str containers'])); formRun = classFactory.formRunClass(args); formRun.init(); formRun.run(); formRun.wait(); }
And then run the job!
Wednesday, October 28, 2015
How to create a self-elevating PowerShell script that will run as administrator every time
Often there are various build processes or other automated tasks that run via PowerShell and need to be run as administrator. If you forget to run it as administrator, it won't work, and you don't always know.
I came across a great blog post by Ben Armstrong that I have to share, where he's created a block of code you just prefix to the beginning of your PowerShell script that will re-launch it as administrator if it is not. Check his post out here:
http://blogs.msdn.com/b/virtual_pc_guy/archive/2010/09/23/a-self-elevating-powershell-script.aspx
Here's a screenshot of the PowerShell code in case his blog goes down:
I came across a great blog post by Ben Armstrong that I have to share, where he's created a block of code you just prefix to the beginning of your PowerShell script that will re-launch it as administrator if it is not. Check his post out here:
http://blogs.msdn.com/b/virtual_pc_guy/archive/2010/09/23/a-self-elevating-powershell-script.aspx
Here's a screenshot of the PowerShell code in case his blog goes down:
Tuesday, October 13, 2015
Without any customization, how to incrementally compile the CIL from the command line
With Dynamics AX 2012, you can start an incremental CIL compile from the command line (or Power Shell) without any customization. This is useful if you have any automated processes that import XPOs frequently and you don't want to constantly build the full CIL.
Create an XML file with this data:
Then save it in a place that is accessible from the AOS service account. I saved it as "C:\IncrementalCIL.xml" on the AOS machine.
Then run this command, subbing in for your environment:
"C:\Program Files (x86)\Microsoft Dynamics AX\60\Client\Bin\Ax32.exe" \\MyNetworkShare\AOS.axc -startupcmd=autorun_C:\IncrementalCIL.xml
Some notes about the XML. The version attribute must not be greater than your system's build, which can be found from calling xInfo::releaseVersion().
There are a TON more autorun features available, and you can use the following links or just dig into \Classes\SysAutoRun.
More information can be found at:
Create an XML file with this data:
<?xml version="1.0" ?> <AxaptaAutoRun exitWhenDone="true" version="6.2" logFile="C:\AxaptaAutorun.log"> <CompileIL incremental="true" /> </AxaptaAutoRun>
Then save it in a place that is accessible from the AOS service account. I saved it as "C:\IncrementalCIL.xml" on the AOS machine.
Then run this command, subbing in for your environment:
"C:\Program Files (x86)\Microsoft Dynamics AX\60\Client\Bin\Ax32.exe" \\MyNetworkShare\AOS.axc -startupcmd=autorun_C:\IncrementalCIL.xml
Some notes about the XML. The version attribute must not be greater than your system's build, which can be found from calling xInfo::releaseVersion().
There are a TON more autorun features available, and you can use the following links or just dig into \Classes\SysAutoRun.
More information can be found at:
- https://msdn.microsoft.com/en-us/library/sysautorun.aspx
- https://msdn.microsoft.com/en-us/library/aa569641.aspx
Thursday, September 10, 2015
How to export all private or shared projects with or without project definitions from a specified layer
I often have to switch development machines, and when I do, I lose all of my private or shared projects. This is a good way to backup your projects and/or their definitions.
static void AKExportProjects(Args _args) { #AotExport TreeNodeIterator tni; ProjectNode projectNode; int exportFlag; Dialog dialog = new Dialog(); DialogField folderName; DialogField projectDefinitionOnly; DialogField exportFromLayer; DialogField projectType; UtilEntryLevel layer; dialog.addText("This will export all projects (shared or private) that exist in a selected model."); projectType = dialog.addFieldValue(enumStr(ProjectSharedPrivate), ProjectSharedPrivate::ProjPrivate); projectDefinitionOnly = dialog.addField(extendedTypeStr(NoYesId), 'Project Definition Only'); folderName = dialog.addField(extendedTypeStr(FilePath)); exportFromLayer = dialog.addField(enumStr(UtilEntryLevel), 'Projects from layer'); dialog.run(); if (dialog.closedOk()) { if (!folderName.value()) throw error("Missing folder"); exportFlag = #export; if (projectDefinitionOnly.value()) exportFlag += #expProjectOnly; layer = exportFromLayer.value(); switch (projectType.value()) { case ProjectSharedPrivate::ProjPrivate: tni = SysTreeNode::getPrivateProject().AOTiterator(); break; case ProjectSharedPrivate::ProjShared: tni = SysTreeNode::getSharedProject().AOTiterator(); break; } projectNode = tni.next() as ProjectNode; while (projectNode) { if (projectNode.AOTLayer() == layer) projectNode.treeNodeExport(folderName.value() + '\\' + projectNode.name() + '.xpo', exportFlag); projectNode = tni.next() as ProjectNode; } } else warning("No action taken..."); }
Monday, August 3, 2015
How to check if a Base Enum has a valid value
Base enums can use integer assignment to be set, but you can set it 0 to any positive valid integer up to 255 inclusive, and that does not mean it's a valid enum.
Take for example the base enum "ABC" (\Data Dictionary\Base Enums\ABC). You can assign ABC=555, and it will store an integer value of 255 with no issue.
To check if an enum value is valid, you can use this method:
Here is a sample job that will demonstrate how this can be an issue:
Take for example the base enum "ABC" (\Data Dictionary\Base Enums\ABC). You can assign ABC=555, and it will store an integer value of 255 with no issue.
To check if an enum value is valid, you can use this method:
static boolean checkABCEnum(ABC _abc) { return new DictEnum(enumNum(ABC)).value2Symbol(_abc)); }
Here is a sample job that will demonstrate how this can be an issue:
static void CheckIfEnumIsValid(Args _args) { // Possible enum values 0, 1, 2, 3 ABC abcValid, abcInvalid; // Valid enum abcValid = ABC::C; info(strFmt("%1, %2", enum2int(abcValid), abcValid)); // Invalid enum, but integer assignment works and is stored abcInvalid = 555; info(strFmt("%1, %2", enum2int(abcInvalid), abcInvalid)); if(new DictEnum(enumNum(ABC)).value2Symbol(abcValid)) info(strFmt("Enum with type %1 and integer value %2 (%3) is valid", typeOf(abcValid), enum2int(abcValid), abcValid)); else error(strFmt("Enum with type %1 and value %2 is invalid", typeOf(abcValid), enum2int(abcValid))); if(new DictEnum(enumNum(ABC)).value2Symbol(abcInvalid)) info(strFmt("Enum with type %1 and integer value %2 (%3) is valid", typeOf(abcInvalid), enum2int(abcInvalid), abcInvalid)); else error(strFmt("Enum with type %1 and value %2 is invalid", typeOf(abcInvalid), enum2int(abcInvalid))); /* Output: 3, C 4, Enum with type Enum and integer value 3 (C) is valid Enum with type Enum and value 4 is invalid */ }
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!
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.
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:
How to find all of a customer/vendor phone numbers with the "Business" role type:
How to find all of a customer/vendor phone numbers and all marked roles:
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:
Here are all of the snippets in one large job you can copy & paste and run (after changing to your own customer)
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.
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):
Please also note the couple comments as they will explain a little more.
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)); }
Subscribe to:
Posts (Atom)