Wednesday, July 31, 2013

Auto-Deploying DLLs and Other Resources - Part 2

In the first post of this series on deploying resources, I discussed the framework and some of its issues we'll have to deal with. In this article, we'll actually write the code to support that article.

Note that I also posted an "intermission" to that blog post based on some reader feedback. The article explains how to use a manually edited (aka hack :-)) Visual Studio project to have AX deploy resources through the AOT VS project framework. It works great, but there's always the possibility that an undocumented "feature" like that may be killed in an update.

So, back to the file deployer framework. We'll create a new class called "CodeCribDeploy" and we'll extend "SysFileDeployment".


As soon as you save the code, you'll notice 4 compile errors complaining you need to implement a few of the abstract methods:
- filename
- parmClientVersion
- parmServerVersion
- destinationPath

You can right-click the class and override each of these. They will still error out on the super() call since that would be calling an abstract method. Just get rid of the super() call for now if (like me) the errors bother you.
Let's start with the method "destinationPath". This indicates where you will store the files you're deploying. This requires some consideration. Users may not be local admins on the machine and may not have enough privileges to put the files anywhere. On the other hand, for DLLs you want to make sure they are in a path where AX will look to load assemblies from. As an alternative to client/bin I like to use the same folder that AX uses to deploy AOT VS project artifacts too, which is in the user's appdata folder as explained in this article. Feel free to change, but for this example that's where we'll put it. So ultimately, my destinationPath method looks like this:

protected FilenameSave destinationPath()
{
    return strFmt(@'%1\%2', j
        CLRInterop::getAnyTypeForObject(System.Environment::GetEnvironmentVariable('localappdata')),
        @'Microsoft\Dynamics Ax\VSAssemblies\');
}


I ask .NET for the System Environment variable "localappdata" and append the folder for the VSAssemblies. Interestingly, the sourcePath() method is not abstract and doesn't need to be overridden. Unfortunately, although it returns the path to the include folder, it runs the code on the client tier and so it returns the wrong value. So, we'll need to write a method to grab the server include folder on the server tier, then change the sourcePath method to return that value. Note I'm using the server include folder (default location is C:\Program Files\Microsoft Dynamics AX\60\Server\[YOURAOSNAME]\bin\Application\Share\Include) because I think that makes sense, but feel free to change this. So this is what we're adding to our CodeCribDeploy class:

protected static server FilenameOpen serverIncludePath()
{
    return xInfo::directory(DirectoryType::Include);
}

protected FilenameOpen sourcePath()
{
    return CodeCribDeploy::serverIncludePath();
}


Next, the filename. Since there's only one filename, this implies you need a class for each file you wish to deploy. I've personally just created a base class with all the overrides, and then just inherit from that for each file, just changing the filename method's return value. So, we'll just enter the filename. In this case I'll deploy "MyDLL.dll".

public Filename filename()
{
    return 'MyDLL.dll';
}


The next two methods to override are "parmClientVersion" and "parmServerVersion". Interestingly these don't seem to be used much by the framework at all. In fact, the only reference is from the base class SysFileDeployment.getClientVersion() and SysFileDeployment.getServerVersion() who seem to just get the version from their parm method. Interestingly, the framework calls the isClientUpdated() method which by default only checks to see if the file exists on the client side. Not helpful. So, let's implement these methods to actually return some useful information on the versions, then we'll fix isClientUpdated to actually use these versions properly. There are different things you can do, including using the .NET framework to get actual assembly version numbers from your DLL, but we'll go with the cheap version and just check timestamps of the files.
Note that we need to run these checks on their respective tiers, ie we need to get the server version by running code on the server tier and the client version by running a check on the client tier. since we're just check file properties (timestamp), we can use the WinAPIServer class to check stuff on the server. Unfortunately, that class demands the FileIOPermission, which means we have the assert that permission on the server tier prior to the calls to WinAPIServer. Since our class will be running client-side, we'll have to create a static server method which we can call from the parmServerVersion.

protected server static anytype ServerFileVersion(str filename)
{
    date serverDate;
    TimeOfDay serverTime;

    new FileIOPermission(filename, 'r').assert();
    
    if (WinAPIServer::fileExists(filename))
    {
        serverDate = WinAPIServer::getFileModifiedDate(filename);
        serverTime = WinAPIServer::getFileModifiedTime(filename);
    }

    return strFmt('%1T%2', serverDate, serverTime);
}

public anytype parmServerVersion()
{
    str filename = strFmt(@'%1\%2', this.sourcePath(), this.filename());

    return CodeCribDeploy::ServerFileVersion(filename);
}

public anytype parmClientVersion()
{
    str filename = strFmt(@'%1\%2', this.destinationPath(), this.filename());
    date clientDate;
    TimeOfDay clientTime;

    if (WinAPI::fileExists(filename))
    {
        clientDate = WinAPI::getFileModifiedDate(filename);
        clientTime = WinAPI::getFileModifiedTime(filename);
    }
    
    return strFmt('%1T%2', clientDate, clientTime);
}


So now we'll override the "isClientUpdated" method to actually perform a version check:

public boolean isClientUpdated()
{
    return this.parmClientVersion() == this.parmServerVersion();
}


Note that here I'm checking if the client and server versions are equal. So if the server version if older, it will return false here and prompt the client to download the older version. That may or may not be what you want.

We also need to make sure the framework picks up on our file to be "checked". It unfortunately doesn't look at subclasses of the base class to determine that automatically. You're supposed to add your classNum as part of a return value of the filesToDeploy() method. If you're reading this and wanting to implement this for AX 2009, you need to over-layer this method and add your class. If you're on 2012, you have a better option: events!
Right-click on your CodeCribDeploy class and click New > Pre- or post-event handler. Let's rename this method to "filesToDeployHandler". We'll get the method's return value, add our class ID to the container, and set the return value back.

public static void filesToDeployHandler(XppPrePostArgs _args)
{
    container filesToDeploy = _args.getReturnValue();

    filesToDeploy += classNum(CodeCribDeploy);

    _args.setReturnValue(filesToDeploy);
}


Finally, we just drag&drop this new method onto the filesToDeploy method of the SysFileDeployer class. Make sure to give the new subscription a meaningful and unique name (or otherwise you'll defeat the clean purpose of using events in the first place). Also make sure to set the properties of the event subscription (right-click your new subscription node, select properties) to "Post" event.


Great, all set, right?! Well, there's one more "fix" we have to perform, as discussed, to make sure our file versions are always checked. To do this, either change the code in the "parmUpToDate" method to always return false, or if you're on AX 2012, again you can use events. By making parmUpToDate return false we force AX to check the versions, as it should. This can be as easy as adding another pre/post handler as we did before, and changing the return value to false.

public static void parmUpToDateHandler(XppPrePostArgs _args)
{
    _args.setReturnValue(false);
}


And obviously we need to drag&drop this onto the parmUpToDate method of the SysfileDeployer class, and set the CalledWhen property to Post.


Make sure to save the whole lot.
Now, when you open a new AX client, you should get the following dialog:


If you don't see it, make sure you put your DLL to be deployed in the right folder, for the right AOS. Yeah, that's what I did.

7 comments:

  1. This comment has been removed by the author.

    ReplyDelete
  2. Hello,

    with CU7, the vsAssembly is now by AOS instance (like AUC file). So to push the file in the correct directory, it's better to use the Standard function "xapplication::getVSAssembliesPath()" to get the VSAssembly folder path than our destinationPath function.

    Charles

    ReplyDelete
    Replies
    1. Indeed it is, haven't had a chance to post that yet. The xApplication::getVSAssembliesPath also exists prior to CU7 which I didn't when writing this article. Thanks for posting.

      Delete
  3. Why did you choose to extend SysFileDeployment when SysFileDeploymentFile and SysFileDeploymentDLL seem like they have much of your same logic? SysFileDeploymentFile for example uses `getFileModifiedDate` just like your code.

    Seems confusing when I think you can just extend one of these classes.

    ReplyDelete
    Replies
    1. Well, it's been more than a year since I wrote this, but I'm sure I had a good reason :-)
      I know I tried to use the DLL for some actual work and ended up overriding more methods than I wanted to.
      But sure, no reason not to use them if they work for you!

      Delete
  4. This article was instrumental is coming up with our final solution. Thanks Joris!

    We ended up using the SysFileDeployment in conjunction with AOT Resources to deploy files that were stored in the Model (not on the server file system).

    I blogged about the solution here: http://coffeestain-it.blogspot.ca/2015/05/dynamics-ax-sysfiledeployment-framework.html

    ReplyDelete
    Replies
    1. Glad it was useful, and thanks for linking your take on this here as well.

      Delete