Extensions are the future – time for a gatekeeper role

The message should have gotten through load and clear by now, customisation of Dynamics 365 for Operations (AX7) should, wherever possible, be done using Extensions. Your development team should have seen this being hammered home by Microsoft and the D365 community over the past months and especially since Microsoft told us that the Application Suite will be sealed by spring 2018. So how do you stop your development team from falling back on old habits by using their tried and test over layering techniques?

Continue reading

Create your own SysBPCheckIgnore macro to avoid code collisions

I’m a firm believer in ensuring that the code that we produce is free of Best Practice errors. I don’t want to evangelise about the benefits of doing this here as there is already enough of that around – just do it! The thing about the best practice checks are that there’s always times when you code something in a particular way knowing that its going to generate a best practice error but knowing that, in this instance, its the right solution. When we need to tell the Best Practice checker that we want it to ignore a specific instance of a Best Practice error, we just have to create an entry in the macro SysBPCheckIgnore. This macro isn’t actually a macro at all, its an XML file that’s consumed by the Best Practice checker to tell it which errors it should ignore. In Dynamics AX 2012 R3 RTM this macro is already over 1200 lines long making it no fun at all to modify – it takes a fairly long time to load into the editor and doing code compares on it is a pretty painful experience. The problem with the SysBPCheckIgnore macro is that just about every solution written for AX has to modify it. This means that every solution will have to perform code merges on the macro when taking upgrades or hotfixes from Microsoft and if you have multiple ISV solutions in your system then you’re certain to get a conflict on this macro and you’ll have to do a code merge to combine all of the solutions changes to the macro.

Utopia

Wouldn’t it be nice if everyone could create their own best practice ignore macro and have the Best Practice check consume that along with the standard macro to build a consolidated ignore list? Unfortunately, there’s no way to do this out of the box and the way that Microsoft engineered the Best Practice checker doesn’t make it easy for us to add in our own macros without modifying the checker itself. I’ve had a functionality change request logged with Microsoft on the Connect site since June 2014 asking them to re-engineer the best practice checker to allow multiple BP Check Ignore macros with little sign so far of it being taken up. You can join me in calling for this change by voting at https://connect.microsoft.com/dynamicssuggestions/feedback/details/904328/allow-multiple-sysbpcheckignore-macros. Rather than wait for Microsoft to change this I thought that I would look at whether I could come up with a solution that could be implemented by everyone without creating any points of code collision. This would mean that it had to be possible to code the solution without making any changes to standard Microsoft code. Happily I found that this is perfectly feasible and requires very little effort – that said, I would still rather see a standard solution from Microsoft as that’s likely to be far more widely adopted than what I’m suggesting below.

Reality

The Best Practice checker is implemented through the classes SysBPCheck, SysBPCheckBase and its sub-classes, all of which is fairly easy to follow. Once I looked at them, I very quickly established that the SysBPCheck.initIgnoreMap() method contains the code that consumes the SysBPCheckIgnore macro. Interestingly, the method already processes two macros (SysBPCheckIgnore and SysBPStyleCheckExceptions) using an Array object that’s loaded with AOT TreeNode references to these two macros. Unfortunately, the Array object is declared locally within the method so we have no way of adding extra elements to the array without modifying the standard code – if only Microsoft had implemented this using a List object declared as a class instance variable with a parm accessor method. No matter which way I looked at it I couldn’t find a way to hook into the code in this method to make it consider my custom best practice ignore macro along with the two that it was already processing.

My solution

Accepting that I couldn’t hook into the existing code directly I decided to look at duplicating the existing code and then triggering it from a post event handler hung off the existing method. Here’s the steps that I went through…

  1. Create a new BP ignore macro adding in <ignorelist> and </ignorelist> tags
  2. Create a new post event handler on class SysBPCheck giving it a unique name (I called mine K3initIgnoreMapPostHandler)
  3. Copy the code shown below into your new post event handler ensuring that you choose your own name for the event handler method (step 2 above) and that you insert the name of your macro at the top of the method in place of the name that I have used
  4. Create a new Event Handler Subscription on the SysBPCheck.initIgnoreMap() method, setting it as a “post” handler and pointing it at the post event handler method that you created in step 2.

Using this approach you can create a BPCheckIgnore macro that’s unique to your solution knowing that, so long as you name it carefully, you’ll never have a code collision with any ISV solution that you choose to use and that you’ll never need to look at the standard SysBPCheckIgnore macro during an AX version or hotfix upgrade. If you’re using multiple ISV solution you might even consider encouraging them to go down this route too so that when you bring all of their models into your solution they can all live together in your system without you needing to do a code merge on the SysBPCheckIgnore macro to combine their changes to the macro. Here’s the code that I used for my post event handler. I’ve chosen to keep the code as close to the standard initIgnoreMap code as I can so it still uses an Array even though there is only one macro loaded into the array. When you create your own copies of this you only need to change the two lines at the top that have have tagged with comments. Remember that you still need to restart the AX client each time you add things to your ignore macro because the Best Practice checker caches the contents of the macros, only loading them the first time that you perform a best practice check.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
public static void K3InitIgnoreMapPostHandler(XppPrePostArgs _args) // Use your own method name here
{
    #define.IgnoreMacro('\\macros\\K3BPCheckIgnore')                // Use your own macro name here
 
    int             errorCode;
    str             errorCodeStr;
    TreeNodePath    path;
    Set             set;
    XmlReader       xmlReader;
    Map             errorCodeMap = new Map(Types::String, Types::Integer);
    TreeNode        macroNode;
    TreeNode        bpIgnoreMacroNode = TreeNode::findNode(#IgnoreMacro);
    Array           macroArray = new Array(Types::Class);
    int             i;
 
    SysBPCheck      sysBPCheck = _args.getThis();
    Map             ignoreMap = SysBPCheck.parmIgnoreMap();
 
    if (!bpIgnoreMacroNode)
    {
        throw error("@SYS28152", #IgnoreMacro);
    }
 
    macroArray.value(1, bpIgnoreMacroNode);
 
    for(i=1;i<=macroArray.lastIndex();i++)
    {
        macroNode = macroArray.value(i);
        xmlReader = XmlReader::newXml(macroNode.AOTgetSource());
        while (xmlReader.read())
        {
            if (xmlReader.name() == #IgnoreListViolation)
            {
                errorCodeStr = xmlReader.getAttribute2(#IgnoreListErrorCode);
                if (errorCodeMap.exists(errorCodeStr))
                {
                    errorCode = errorCodeMap.lookup(errorCodeStr);
                }
                else
                {
                    //BP Deviation Documented
                    errorCode = runbuf(strFmt('int convert(){#SysBPCheck return %1;}', errorCodeStr));
                    errorCodeMap.insert(errorCodeStr, errorCode);
                }
 
                path = xmlReader.getAttribute2(#IgnoreListPath);
 
                if (ignoreMap.exists(errorCode))
                {
                    set = ignoreMap.lookup(errorCode);
                }
                else
                {
                    set = new Set(Types::String);
                    ignoreMap.insert(errorCode, set);
                }
                set.add(path);
            }
        }
    }
}