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.

Tuesday, July 30, 2013

Custom Query Range Functions using SysQueryRangeUtil

You've probably seen these requests before. Users want to submit some report or other functionality to batch, and the query should always be run for "yesterday". It's a typical example where, as a user, it would be handy to be able to use functions in your query range. Well, you can. And in fact, you can make your own, very easily!

Enter the class "SysQueryRangeUtil". All it contains is a bunch of static public methods that return query range values. For example, there is a method called "day" which accepts an optional integer called "relative days". So, in our example of needing a range value of "tomorrow", regardless of when the query is executed, you could use day(-1) as a function. How this works in a range? Just open the advanced query window and enter your function call within parentheses.

Let's make our own method as an example. Add a new method to the SysQueryRangeUtil class, and enter the following, most interesting code you've ever encountered.

public static str customerTest(int _choice = 1)
{
    AccountNum accountNum;
    
    switch(_choice)
    {
        case 1:
            accountNum = '1101';
            break;
        case 2:
            accountNum = '1102';
            break;
    }
    
    return accountNum;
}


So, this accepts an options parameter for choice. If choice is one (or choice is not specified), the function returns 1101, if 2 it returns 1102. Save this method and open a table browser window on the CustTable table. Type CTRL+G to open the grid filters. In the filter field for the AccountNum field, enter: (customerTest(1)).


So, the string returned from the method is directly put in the range. So, you could do all sort of interesting things with this, of course. Check out some of the methods in the SysQueryRangeUtil as examples.

Thursday, July 18, 2013

Auto-Deploying DLLs and Other Resources - Intermission

I posted part 1 of the auto-deploying DLLs and other resources article last month. Although I will finish the part 2 article as promised, an interesting comment and subsequent email discussion / testing has prompted me to include this "intermission".

The deployment framework has existed throughout quite a few versions of AX. when AX 2012 was released and we were all drooling over the Visual Studio projects in the AOT, one thing became clear: referenced DLLs within the project are not deployed like the DLLs built from the project. I tried quite a few options in the properties of the references to get the DLLs copied to the output folder etc, but nothing worked. Additionally, deploying other files from your project (images etc) doesn't work either.
But, one attentive reader of this blog, Kyle Wascher, pointed out a way to edit your Visual Studio project file to have it deploy files to the output folder. Interestingly, AX honors these settings as opposed to honoring the regular properties in the VS project file. So, here's how you do it!


First, let's create a new project in Visual Studio 2010. I'm choosing the Class Library project type, and I'm naming it "DeploymentProject".



Once created, right-click the new project and select "Add DeploymentProject to AOT".



Right-click on your project and select "Properties". Make sure to select "Deploy to client" (or deploy to server or client or EP or all of them, depending on your scenario). For this test I'll just set Deploy to client to YES.



Of course, we need a DLL to deploy. I'm going to create a new project/solution but of course that is NOT a requirement, you can pick any DLL you have created anywhere or downloaded from third parties. Shift-click on Visual Studio in your Windows taskbar to start another instance of Visual Studio. Create new project,again I pick the Class Library project type, and I'm naming it MyDLL. After this, my project looks like this. Again, creating this new project is just an illustration of a DLL we'll deploy, it's not needed to make this work. As an illustration for later, MyDLL contains a public class MyClass with a public static method "Message" that returns the string "Hello, world!". Since the code is irrelevant I'm just putting a screenshot up here. On a side note, it seems if you create another project within the solution where you create the AX VS project, the new project will also be added to the AOT, which of course defeats what we are trying to do here.




Make sure to build this DLL so we can use it.

Ok, so now there are two ways to have the DLL be part of your project. One, you add it as an actual reference. Or two, you just add it to your project as a regular file. In this example, I will add the DLL as a reference in the project. This will allow me to actually also use the DLL in the project itself, which I will use further down as an example. This is also the most common scenario where one needs to deploy an extra DLL.
So, go back to your AX VS Project "DeploymentProject", right click the references node in your deployment project, and click "Add reference". On the "Add Reference" dialog click the Browse tab and navigate to the MyDLL.dll we built in the other project. You'll find that DLL file in your project's folder under bin/debug or bin/release depending on which configuration you used to build.




Ok, open the File menu and select "Save all" to make sure we've saved our project. Time to get our hands dirty and "hack" the Visual Studio project :-) Right-click on your project and select "Open folder in windows explorer" (or manually browse to your project folder). Find your .csproj file (in my case it's DeploymentProject.csproj) and open it in notepad or your favorite text editor (depending on your OS you may or may not have a "Open with" option, you may have to right-click or shift-right-click, it all depends... if all else fails, just open notepad and open the file from within notepad). Find the XML nodes called ItemGroup and add your own ItemGroup as follows:



A few things there. By using $(TargetDir) as the path, we're telling Visual Studio/AX to find our extra DLL in the folder where this CURRENT project's DLL is being built. This is important, since it will make sure that wherever the project is compiled, we'll always find MyDLL.DLL correctly. By default when you add a reference, VS will set the "copy local" flag to yes, which will make sure the referenced DLL is available. Save the csproj file and switch back to Visual Studio. You should get the following dialog:



This is exactly what we need. Click "Reload". Now, we've edited the VS project on disk but it's not in the AOT yet. Normally, VS updates the AOT any time you click save. Unfortunately, in this case you just reloaded the project from disk so VS doesn't actually do anything when you hit save, as it doesn't think you've changed anything. So, the easiest thing you can do is just click the "Add DeploymentProject to AOT" option again, as we did in the beginning. This will force an entire update of the project to the AOT.
Ok, so now (remember I had "Deploy to Client" set to yes) I will just open the AX client. And as explained in my article on VS Project DLL deployment, you should see both the DeploymentProject.dll and the MyDLL.dll files appearing in your Users\youruser\AppData\Local\Microsoft\Dynamics Ax\VSAssemblies.

Now, as for using the code of MyDLL. Remember we added a class and method there. However, the DLL is referenced in the VS project, but not in the AOT. So, your code inside DeploymentProject.dll can use that class, but your X++ can only use the code from DeploymentProject.dll. If you need access to the MyDLL.dll code from X++, you will need to manually add a reference to that DLL in the AOT still. Now, at that point you point it to the location of the DLL, but at runtime (or after you deploy the code to a test or production environment using models) AX will just try to load the DLL from its known search paths, which will include the VSASSemblies folder in your appdata. So as long as you include the AOT reference as part of your model, this trick will work everywhere.

As a final note, you can use this to deploy ANY file. You can right-click your project and select "add / existing item" and select a JPG file for example. In the properties of the file, make sure to set the "Copy to Output Directory" flag to "Copy always". Then, just add another VSProjectOutputFiles node in your csproj file.