MSDyn365FO. How to playback a task recording from X++.

s-l300

Warmup of a VM is an old issue and everyone tries to find a workaround. It is critical for demo VMs that are turned off most of the time and you don’t want to demo a product that is slow as hell.

Well-known workaround is to manually open all the forms you are going to use but it’s boring and time-consuming task. One of the yammer users came up with an idea to use task recording and here is an implementation in its dirtiest way (calling private methods on object without any validation) that is good enough for demo anyway, so feel free to improve this code:

class WarmUpRunnableClass
{
    public static void main(Args _args)
    {
        var currentRecording = SysBPMHelper::LoadLocalizedRecordingFromLCS(80244103); //BpmLineId here.

        Args args = new Args();
        args.name(formstr(SysTaskRecorderPane));

        FormRun formRun = ClassFactory.formRunClass(args);
        formRun.init();
        formRun.run();
        formRun.loadRecording(currentRecording);
        formRun.maintainExistingRecording();
        formRun.playToStep(100000);
        formRun.wait();
    }
}

This runnable class could be executed from URL, so you can add it to Windows Task Scheduler and execute it on the VM start automatically.

80244103 is a task recording id that is hardcoded here for simplicity. Previously I recorded it and saved to BPM. You can improve this code and add some smarties to grab it from BPM using search by keyword or any other logic you like.

For those who think that hardcode is good enough to get this number you need to go to LCS, open BPM and find task recording you want to run. PLID value from URL is a number you need:

PLID.png

 

Advertisements

MSDyn365FO. How to switch from IIS Express to IIS on development VM.

All new environments starting from PU12 are using IIS Express in debug mode and often due to random issues we have to restart it using “Restart IIS Express” option in the Dynamics 365 menu. There are multiple discussions on community and yammer forums regarding these issues and Joris de Gruyter mentioned that it is possible to switch back to IIS if you have admin access (environment deployed to customer’s or partner’s own Microsoft Azure subscription), but did not explain how 🙂

All we need to do is to edit DynamicsDevConfig.xml file that could be found in ‪K:\AosService\PackagesLocalDirectory\Bin folder (drive letter could be different for your VM). Change RuntimeHostType value from IISExpress to IIS and that’s all.

DynamicsDevConfig

You may find same file under %userprofile%\Documents\Visual Studio 2015\Settings\ , however, Visual Studio rolls back any changes to RuntimeHostType if value is different to value in \AosService\PackagesLocalDirectory\Bin\DynamicsDevConfig file, so don’t try to change it for individual user.

D365FOE. How to override form data source field lookup method.

A long time ago, I wrote a blog post explaining how to override form data source field methods. I skipped lookup, most popular method, because it did not work and logged a bug with MS hoping that it would be fixed soon. It took more time than I expected, but finally, it works!

Here is a small example how to override it. I’m going to use approach similar to previous post, full example is available on GitHub. Version I’m using is 7.3 PU13.

/// <summary>
/// Handles events raised by <c>SalesTable</c> form.
/// </summary>
public class SalesTableEventHandler
{
    /// <summary>
    /// Post event handler for <c>SalesTable</c> <c>SalesLine</c> Initialized event.
    /// </summary>
    /// <param name=“_sender”></param>
    /// <param name=“_e”></param>
    [FormDataSourceEventHandler(formDataSourceStr(SalesTable, SalesLine), FormDataSourceEventType::Initialized)]
    public static void SalesLine_OnInitialized(FormDataSource _sender, FormDataSourceEventArgs _e)
    {
        var overrides = SalesTableFormExtensionOverrides::construct();

        _sender.object(fieldNum(SalesLine, ItemId)).registerOverrideMethod(methodStr(FormDataObject, lookup),
            methodStr(SalesTableFormExtensionOverrides, ItemId_OnLookup), overrides);
    }
}

/// <summary>
/// Contains methods which are used to override <c>SalesLine</c> data source field methods.
/// </summary>
public class SalesTableFormExtensionOverrides
{
    protected void new()
    {
    }

    /// <summary>
    /// Constructs a new instance of <c>SalesTableFormExtensionOverrides</c> class.
    /// </summary>
    /// <returns>
    /// A <c>SalesTableFormExtensionOverrides</c> class.
    /// </returns>
    public static SalesTableFormExtensionOverrides construct()
    {
        return new SalesTableFormExtensionOverrides();
    }

    /// <summary>
    /// Provides a lookup for the <c>InventTable</c> table
    /// </summary>
    /// <param name = "_callingControl">
    /// The form string control object with which to perform the lookup.
    /// </param>
    public void ItemId_OnLookup(FormStringControl _callingControl)
    {
        SysTableLookup sysTableLookup = SysTableLookup::newParameters(tableNum(InventTable), _callingControl);

        sysTableLookup.addLookupfield(fieldNum(InventTable, ItemId));
        sysTableLookup.addLookupfield(fieldNum(InventTable, NameAlias));

        sysTableLookup.performFormLookup();
    }
}

As you may notice, signature of this method is different to signature of other methods, like validate or jumpRef. They accept FormDataObject as a parameter and lookup() accepts FormStringControl. It looks a bit inconsistent for me and we need to be extra careful here because you can register override method with any signature and get error in the run-time.

D365FO. Working with Azure File storage.

Azure Storage - Files

Current version of AX uses Azure Blob storage for various things like document handling, retail CDX files, DIXF and Excel add-in. You can find several blogs explaining how to upload and download files to Blob, SharePoint or temporary storage. However, what about file shares?

Azure File storage implements SMB 3.0 protocol and could be easily mapped to your local computer. You need just a few minutes to create new storage account and mount it, watch this how-to video for details.

To read file from newly created share we can use next code:

using Microsoft.Azure;
using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Blob;
using Microsoft.WindowsAzure.Storage.File;

class RunnableClass1
{
    public static void main(Args _args)
    {
        System.IO.MemoryStream memoryStream;

        var storageCredentials = new Microsoft.WindowsAzure.Storage.Auth.StorageCredentials('AzureStorageAccountName', 'AzureStorageAccountKey');

        CloudStorageAccount storageAccount = new Microsoft.WindowsAzure.Storage.CloudStorageAccount(storageCredentials, true);

        CloudFileClient fileClient = storageAccount.CreateCloudFileClient();

        CloudFileShare share = fileClient.GetShareReference('AzureStorageShareName');

        if (share.Exists(null, null))
        {
            CloudFileDirectory rootDir = share.GetRootDirectoryReference();

            CloudFileDirectory fileDir = rootDir.GetDirectoryReference('folder');

            if (fileDir.Exists(null, null))
            {
                CloudFile file = fileDir.GetFileReference('file.txt');

                if (file.Exists(null, null))
                {
                    memoryStream = new System.IO.MemoryStream();
                    file.DownloadToStream(memoryStream, null, null, null);
                }
            }
        }
    }
}

References:

Edited:

Azure File Storage client source code could be found on GitHub

D365O. Trick to pass a value between Pre and Post event handler using XppPrePostArgs.

Recently we came across a scenario where we needed to check if a field has changed after super() in update method of a table. Back in the days of AX 2012 you could easily compare original field’s value with current using orig() method before super() and call necessary logic after.

public void update()
{
    boolean myFieldHasChanged = this.MyField != this.orig().MyField;

    super();

    if (myFieldHasChanged)
    {
        this.doStuff();
    }
}

Now we want to do the same using extensions. We can create Pre and Post event handlers, but they are static, so we need a way to pass a value between them.
First option is to use static field, like it’s done in RunBase extension example

public static class MyTableEventHandler
{
    private static UnknownNoYes myFieldHasChanged;

    [PreHandlerFor(tableStr(MyTable), tableMethodStr(MyTable, update))]
    public static void MyTable_Pre_update(XppPrePostArgs _args)
    {
        MyTable myTable = _args.getThis() as MyTable;

        if (myTable.MyField != myTable.orig().MyField)
        {
            MyTableEventHandler::myFieldHasChanged = UnknownNoYes::Yes;
        }
        else
        {
            MyTableEventHandler::myFieldHasChanged = UnknownNoYes::No;
        }
    }

    [PostHandlerFor(tableStr(MyTable), tableMethodStr(MyTable, update))]
    public static void MyTable_Post_update(XppPrePostArgs _args)
    {
        MyTable myTable = _args.getThis() as MyTable;

        if (MyTableEventHandler::myFieldHasChanged == UnknownNoYes::Yes)
        {
            myTable.doStuff();
        }

        MyTableEventHandler::myFieldHasChanged = UnknownNoYes::Unknown;
    }
}

Another option is to use XppPrePostArgs as a vehicle for new parameter. XppPrePostArgs has collection of parameters under the hood, so nothing stops us to add one more and framework will take care of passing it between Pre and Post event handler!

XppPrePostArgs_collection.jpg

public static class MyTableEventHandler_XppPrePostArgs
{
    const static str myFieldHasChangedArgName = 'myFieldHasChanged';

    [PreHandlerFor(tableStr(MyTable), tableMethodStr(MyTable, update))]
    public static void MyTable_Pre_update(XppPrePostArgs _args)
    {
        MyTable myTable = _args.getThis() as MyTable;

        boolean myFieldHasChanged = myTable.MyField != myTable.orig().MyField;

        <strong>_args.addArg(MyTableEventHandler_XppPrePostArgs::myFieldHasChangedArgName, myFieldHasChanged);</strong>
    }

    [PostHandlerFor(tableStr(MyTable), tableMethodStr(MyTable, update))]
    public static void MyTable_Post_update(XppPrePostArgs _args)
    {
        MyTable myTable = _args.getThis() as MyTable;

        <strong>boolean myFieldHasChanged = _args.getArg(MyTableEventHandler_XppPrePostArgs::myFieldHasChangedArgName);
</strong>
        if (myFieldHasChanged)
        {
            myTable.doStuff();
        }
    }
}

Using one of these approaches you should remember that static fields apply to the class, not to instances of the class, so do not mix well with concurrency. Trick with XppPrePostArgs tightly depends on current implementation, that could be changed anytime and comes with no warranty.

To overcome this and other limitations of extensions capabilities Microsoft is introducing Method wrapping and chain of command and I’m pretty sure that we’ll see blogs on this from my MVP fellows soon.

D365O. How to deploy Demo VM using Visual Studio subscription.

Many of us have Visual Studio subscription (formerly called MSDN subscription) and there is monthly Azure credit coming with it. It could be used to spin up D365O demo VM, but the whole process is not straightforward. I would describe it step by step today.

  1. Activate monthly Azure credit. You will get an email with subscription id after completion.ActivateSubscription.png
  2.  Go to Azure portal where you can check details of new subscription.AzurePortal.jpgAzurePortalSubscription.jpg
  3. Different types of subscriptions give you different amount, enterprise is the best (but costs a lot) and gives you 150$ monthly credit, that is enough to run D12V2 VM for 235 hours per month.
  4. With new subscription you get new tenant. Now we need to activate D365 trial on this tenant. That’s a mandatory step for ARM onboarding. Existing customers can follow steps on the customer source  to request access via email. Partners can find details on the partner source , you have 2 options there: request new tenant with trial or activate trial on existing tenant. We will activate trial because we already have a tenant with subscription, but it’s possible to transfer subscription to new tenant as well.
  5. To activate trial on existing tenant
    1. Create new user .AzureActiveDirectoryAddUser.jpg
    2. Make new user Owner of the subscriptionAzureSubscriptionOwner.jpgAzureSubscriptionOwnerNew.jpg
    3. Make new user Global admin of Active Directory.AzureActiveDirectoryUserRole.jpg
    4. Follow a link to trial offer from partner source article. You need to login using credentials of newly created user.ActivateTrial.jpgconfirmtrial
  6. No we are ready to create new LCS project. Go to LCS and login using user created on step 4. LCSNewProject.jpg
  7. Fill all the details required: Version of AX, project name and methodology.LCSNewProjectDetails.jpg
  8. Setup connection to Azure.LCSMicrosoftAzureSettings.jpg
  9. In the organizations list you will see tenants of project users. You can invite users from different tenants to a project and you will see their organization in the list as well.  lcsnewprojectauthorizeCurrent user should be administrator of the tenant to complete authorization.LCSNewProjectAuthorization.jpgLCSNewProjectAuthorizationComplete.jpg
  10. Add Dynamics Deployment Services [wsfed-enabled] to the subscriptionAddDynamicsDeploymentServices.jpg
  11. Setup Azure connector. Check “Configure to use Azure Resource Manager” checkbox or you won’t be able to deploy any VM starting from Update 2. Azure Subscription Id and tenant name could be found on Azure portal.LCSSetupAzureConnector.jpgLCSSetupAzureConnector2.jpgLCSSetupAzureConnector3.jpg
  12. For ARM deployments we don’t need to download any certificate, so just click “Next” on this step.lcssetupazureconnector4
  13. Select Azure region. Please note that not all regions are available for Visual Studio Subscriptions. If you select wrong region you will have to create new connector with another region because all deployments would fail.LCSSetupAzureConnectorRegion.jpg
  14. Deploy new Demo VMLCSDeployNewVM.jpgLCSDeployNewVM2.jpgLCSDeployNewVM3.jpg
  15. Enter VM name and go to Advance settingsLCSDeployNewVM5.jpg
  16. Select demo data package or “None” for blank environment. Here you could select demo data from ISV partners as well if they shared it with you.LCSDeployNewVM6.jpg
  17. Select number of disks. Select 7 if you want to scale it down to D12V2 later, otherwise you would have to manually delete them, that is not an easy task, because it’s not possible to change size if target VM has different disks configuration. LCSDeployNewVM7.jpg
  18. Select GER configuration to be deployed. You can deploy any later from assets library if you missed this step.LCSDeployNewVM8.jpg
  19. Select VM size. Different sizes have different hardware configuration and costs. To find costs you can use Pricing calculator. I prefer to use D13V2 and D12V2 because I think they have best  price–performance ratio.LCSDeployNewVM9.jpg

That’s all. Usually deployment takes up to 5 hours. Don’t forget to setup auto shutdown to save your costs!

Links

AX 7. Trick to override method in derived class without overlaying.

Scenario:

The VendDocumentLineType class has several subclasses that handle specific types of vendor document lines. We want to change a behaviour for invoice lines and implement new logic for Purchase Unit field defaulting.

Solution using overlaying:

Overlay VendDocumentLineType_Invoice class and override determineDefaultPurchUnit method.

Solution without overlaying:

venddocumentlinetypehierarchy

1) Create class derived from VendDocumentLineType_Invoice:

[Form]
/// <summary>
/// The <c>VendDocumentLineType_Invoice_Sample</c> class is used for validation and applying default values to invoice lines.
/// </summary>
class VendDocumentLineType_Sample extends VendDocumentLineType_Invoice
{
}

2) Override determineDefaultPurchUnit method:

protected PurchUnit determineDefaultPurchUnit()
{
    // implement new behaviour
    return VendParameters::find().DefaultPurchUnit_Sample;
}

3) Create post event handler for VendDocumentLineType constructor to replace VendDocumentLineType_Invoice with new implementation:

/// <summary>
/// Handles events raised by <c>VendDocumentLineType</c> class.
/// </summary>

public static class VendDocumentLineTypeEventHandler_Sample
{
    /// <summary>
    /// Post event handler for VendDocumentLineType.createFromTable(...)
    /// </summary>
    /// <param name="_args"></param>
    [PostHandlerFor(classStr(VendDocumentLineType), staticMethodStr(VendDocumentLineType, createFromTable))]
    public static void VendDocumentLineType_Post_createFromTable(XppPrePostArgs _args)
    {
        // get params from args
        VendDocumentLineMap vendDocumentLineMap = _args.getArg(identifierStr(_vendDocumentLineMap));
        PurchLine           purchLine           = _args.getArg(identifierStr(_purchLine));
        PurchParmUpdate     purchParmUpdate     = _args.getArg(identifierStr(_purchParmUpdate));

        // construct and init new object
        VendDocumentLineType strategy = VendDocumentLineType_Invoice_Sample::createFromTable(vendDocumentLineMap, purchLine, purchParmUpdate);

        // replace return value only if new strategy is created; otherwise use standard
        if (strategy)
        {
           // replace return value with new implementation
            _args.setReturnValue(strategy);
        }
    }

}

4) Implement methods that will instantiate and initialize VendDocumentLineType_Invoice_Sample class:

/// <summary>
/// Constructs a new instance of a <c>VendDocumentLineType_Invoice_Sample</c> class derivative.
/// </summary>
/// <param name="_vendDocumentLineMap">
/// A <c>VendDocumentLineMap</c> record.
/// </param>
/// <param name="_purchLine">
/// A <c>PurchLine</c> table record that is used when you apply the default values; optional.
/// </param>
/// <param name="_purchParmUpdate">
/// A <c>PurchParmUpdate</c> table record that is used when you apply the default values; optional.
/// </param>
/// <returns>
/// A <c>VendDocumentLineType_Invoice_Sample</c> class.
/// </returns>
public static VendDocumentLineType_Invoice_Sample createFromTable(VendDocumentLineMap _vendDocumentLineMap, PurchLine _purchLine = null, PurchParmUpdate _purchParmUpdate = null)
{
    VendDocumentLineType_Invoice_Sample strategy;

    // VendDocumentLineType_Invoice is created only for this 2 types
    if (_vendDocumentLineMap.Ordering == DocumentStatus::Invoice ||
        _vendDocumentLineMap.Ordering == DocumentStatus::ApproveJournal)
    {
        strategy = new VendDocumentLineType_Invoice_Sample();
        strategy.init(_vendDocumentLineMap, _purchLine, _purchParmUpdate);
    }

    return strategy;
}

public void init(VendDocumentLineMap _vendDocumentLineMap, PurchLine _purchLine, PurchParmUpdate _purchParmUpdate)
{
    // code copied from VendDocumentLineType::createFromTable to init new instance
    this.vendDocumentLineMap(_vendDocumentLineMap);
    this.purchLine(_purchLine);
    this.physicalStrategy(VendDocumentLineTypePhysical::createFromTable(this, _vendDocumentLineMap));
    this.purchParmUpdate(_purchParmUpdate);
}

You can apply this approach to any class hierarchy in AX that has construct method (like SalesLineType, PurchLineType, etc.) when you need to override methods in derived class to implement new behaviour.