Dynamics AX SysFileDeployment Framework - Deploy files automatically (using resources) to the client on login

Sometimes when creating a customization in AX, external files are required to make things happen.  External graphics files, XML or even DLL's may be required by the AX client.

For example, a recent customization for my company required a control in AX to drag-select areas of a PDF document, so the user could tell the system where relevant information is located on the document.  AX is far too limited to satisfy this requirement with native UI controls, so I created a .NET managed user control to complete the task.  This control (DLL file) is required to be installed on each client before the form that uses it can be loaded.

Herein lies the problem.

If your company has only one or two clients connecting to the AOS server, you may be able to just install these missing components manually.  However, when you have hundreds of clients connecting to your server, this quickly becomes a maintenance nightmare of ensuring clients are up to date.

The SysFileDeployment framework in AX exists to solve this very problem.  Essentially, its purpose is to ensure any 3rd party files missing on the client (be it DLLs, images, XML files, or whatever) are automatically downloaded from the server to the client when the user logs in.  Awesome right?  Well, yes, except for a few small details:

  1. The source file(s) need to be installed on the server so it can serve the files(s) to the client.  This can be a real problem for ISV's developing stand alone models (i.e. now you require an installer of some sort)
  2. The framework is designed to run only once during the entire LIFETIME of the client.  If the files are upgraded on the server, and the client had previously downloaded them via this framework, the updated files will never be downloaded.
Joris de Gruyter has a great two part article that goes into detail about the SysFileDeployment framework.  I encourage you to read those articles.  He describes the very same problems I have had above.

However, the base implementation does not work well for ISV's, specifically around point 1 above.  For our purposes, we wanted to package up everything in a single XPO or Model, and have everything Just Work™.

The core problem is this: how do we store a file (or files) in the AOT so that we can gain the benefit of the import/export feature of XPO's.  The answer of course, is Resources!

The resource node of the AOT is designed exactly for this purpose; to store a binary or text file that can easily be transported with a model or XPO.  Simply right click, select Create from File, and choose the file you wish to add.
The SysResource class can be used to get the resource and save it to a file:

static void Coffee_SaveResourceToFile(Args _args)
{
    resourceNode    resourceNode;
    str             fileName;

    resourceNode    = SysResource::getResourceNode(resourceStr(Coffeestain_dll));
    fileName        = SysResource::saveToTempFile(resourceNode);

    info("The file was saved to " + fileName);
}

Now we just need to write the code for the SysDeploymentFramework.

The framework consists of four main classes:

 
  • SysFileDeployer: The main class, responsible for launching the file deployment loop
  • SysFileDeployment: An abstract class that represents a single file to deploy.  If must be overriden
  • SysFileDeploymentFile: An abstract class used to deploy a single file (XML, Image, etc).  It extends SysFileDeployment
  • SysFileDeploymentDLL: An abstract class used to deploy a single DLL.  The DLL is registered during the deployment process.  It extends SysFileDeploymentFile
While Joris recommends extending SysFileDeployment directly, I find there is little reason not to use the SysFileDeploymentFile class.  When SysFileDeploymentFile is extended, a few abstract methods must be overridden:

class SysFileDeploymentFile_Coffeestain extends SysFileDeploymentFile
{
}


  • destinationPath: The directory to copy the file to
  • filename: The filename of the file to copy

Of note here is that sourcePath is not required to be overridden.  It defaults to C:\Program Files\Microsoft Dynamics AX\60\Server\[Instance (MicrosoftDynamicsAX by default)]\bin\Application\Share\Include.  Even though not required, you can still override this to point to a directory of your choosing.

However, this won't help when deploying a resource as a file.  For this, I created my own SysFileDeployment class, mirroring SysFileDeploymentFile:

///

///    The SysFileDeploymentResource class deploys a resource file from the resource AOT node
/// 

/// 
///    Extend this class when you want to deploy a Resource file.
/// 

abstract class SysFileDeploymentResource extends SysFileDeploymentFile
{
    FilePath        sourcePath;
    resourceNode    resourceNode;
}

We need the developer to specify the resource name, so creating a new abstract method will force this requirement:

protected abstract ResourceName resourceName()
{
}

At this point we need to provide a sourcePath to the framework.  If we use the method above (SysResource::saveToTempFile()) we already know the source path; it is the path in which we save the temporary file.  We can override the sourcePath method of the base class for the developer so this is abstracted away.

protected FilenameOpen sourcePath()
{
    return sourcePath;
}

However the sourcePath variable will need to be set.  In order to do that, we need to save the temporary file at the time that this class runs.  Normally we could just toss some code in the New() method.  However, because this class is abstract, and because of the way the framework instantiates these classes, New() is not called in some cases, causing our initialization code not to run.

For my implementation, I've chosen to initialize and save the file when retrieving the resourceNode.

protected resourceNode resourceNode()
{
    if(!resourceNode)
    {
        resourceNode = SysResource::getResourceNode(this.resourceName());
        if(!resourceNode)
        {
            throw error(strFmt("Resource '%1' not found", this.resourceName()));
        }

        resourceNode.AOTload();

        //This is the first method that runs, We need to create the file on the server
        //(by saving the resource binary) if it does not yet exist.  Do so now.
        sourcePath = SysFileDeploymentResource::SaveResourceTempFile(resourceNode);
    }

    return resourceNode;
}

The sourcePath implementation now changes to:
protected FilenameOpen sourcePath()
{
    //This call also sets the source path variable
    resourceNode = this.resourceNode();
    return sourcePath;
}

Last is to implement the call to SaveResourceTempFile().  Normally, we could just call SysResource::saveToTempFile(), however we need this action to happen exclusively on the server.  There are two reasons why the saveToTempFile method won't work.

The first is that the method needs to be marked with the 'server' modifier to ensure it runs on the server.  The second (and this is common in many places in core AX code) is that the call uses the WinAPI class.  This class is explicitly marked to run on the client and not the server (WinAPIServer is used for that).

If we run saveToTempFile while on the server, the WinAPI call checks for the existence of the file ON THE CLIENT and fails.  To overcome this, I chose to re-write the saveToTempFile call.

private static server str SaveResourceTempFile(resourceNode _resourceNode)
{
    FilePath            filePath;
    Filename            filename;
    BinData             bin;
    FileIOPermission    fileIOPermission;
    TextBuffer          textBuffer;

    if(!_resourceNode) return "";

    //Loads the node into memory.  Without this call properties (such as filename) are blank
    _resourceNode.AOTload();

    filePath = SysResource::getTempPathName();
    if (filePath && strFind(filePath, '\\', strLen(filePath), 1) == 0)
    {
        filePath += '\\';
    }

    filename = filePath + _resourceNode.filename();

    //Binary file
    try
    {
        bin = new BinData();
        bin.setData(SysResource::getResourceNodeData(_resourceNode));

        fileIOPermission = new FileIOPermission(filename, 'rw');
        fileIOPermission.assert();

        //BP Deviation Documented
        bin.saveFile(filename);
    }
    catch
    {
        //Text file
        try
        {
            textBuffer = new TextBuffer();
            textBuffer.setText(conPeek(SysResource::getResourceNodeData(_resourceNode), 1));

            fileIOPermission = new FileIOPermission(filename, 'rw');
            fileIOPermission.assert();

            //BP Deviation Documented
            textBuffer.toFile(filename);
        }
        catch
        {
            error(strFmt("@SYS19312", filename));
        }
    }

    return filePath;
}

That's it.  The framework is complete (aside from hooking up the event handlers, which I won't explain here, as the posts by Joris explain this is detail).

Now, we just need to implement a class for each resource we want to deploy.  We need to extend SysFileDeploymentResource, and implement 3 methods:

protected ResourceName resourceName()
{
    //The name of the resource file you want to deploy
    return resourceStr(Coffeestain_dll);
}

public Filename filename()
{
    //This must be the same for each class
    //We can't move this to the base class due to the way the framework is coded
    return this.resourceNode().filename();
}

protected FilenameSave destinationPath()
{
    //Any destination path you want here
    return xInfo::directory(DirectoryType::Bin);
}

Note: Since filename here never changes, it would have been smarter to add this implementation onto the base class so it is abstracted away.  However there is a bit of code in the framework that essentially looks for this method on every class that extends SysFileDeployment and attempts to create an instance of that class.  Because SysFileDeploymentResource is abstract, the framework crashes and burns.  What should be happening is the framework should also check if the class CAN be instantiated before it attempts to do so, but I digress.

Now when you open the client, you should get a popup such as this one:

Clicking OK will initiate the download from the server to the client.  Barring any unforeseen errors (access rights being at the top of the list, (run AX as an Administrator to handle 90% of these)), the client is now up to date with the new file(s), neatly contained in an exportable project.







This has been a lengthy article.  If you want to give it a try yourself, download the sample project here.

Comments

  1. Hi,

    Thanks for sharing this. I have imported and compiled this project and it runs pretty much fine. Though when I am running the job Coffee_RunSysFileDeploymentFramework or when I restart the client. I getting an error "The object could not be created because class SysFileDeploymentResource is abstract." Any idea how to fix same?

    ReplyDelete
    Replies
    1. This error occurs when you try to override the filename() method on the abstract class (in this case SysFileDeploymentResource).

      The problem is in \Classes\SysFileDeployment\isNameValid, for every class that extends SysFileDeployment, the code checks for the existance of a method with the name "filename". If the method exists, it attempts to create a concrete instance of that class in order to call the method "filename". Because SysFileDeploymentResource is an abstract class, this process fails with the message you described.

      In order to work around this issue, do not override the filename() method in SysFileDeploymentResource, only override it in each of classes that extend SysFileDeploymentResource.

      Delete
  2. I was wondering what your deployment technique is for .dll files that are not part of an AX VS project (so they don't get deployed automatically to the VSAssemblies), but you want to deploy them server side, so that server side code can use the dll.
    e.g. a third party dll you want to use in batch.

    ReplyDelete
    Replies
    1. Hi Sven,
      For server side .dll files, your best bet is to create your own installer .msi that manually copies the .dlls to the GAC or server bin.

      Ideally, for ISVs, we would want to create a custom axupdate.exe, mirroring Microsoft's hotfix installer and strategy, however I have not found any documentation on extending that framework.

      Delete
    2. I guess I should also mention, it is possible to create a "dummy" .NET project, add the .dlls as references to that project, and then deploy to the AOT. This in theory would force the referenced .dlls to get copied to the client's bin directory.

      Having said that, I've never gotten the results I've wanted out of the process. Either the .dlls never get copied, or they are in the wrong directory. Either way, the .msi installer is a far easier (albeit manual) solution.

      There is a good article on the subject here: http://daxmusings.codecrib.com/2013/07/auto-deploying-dlls-and-other-resources.html

      Delete

Post a Comment

Popular posts from this blog

SQL Reporting Services - Viewer broken in Non-IE Browsers: FIX

Dynamics AX 2012 - Visual Studio Team Services Custom Work Item Fields Break Version Control (Error TF237121)