Playing Peekaboo with AX modules

Posted: April 8, 2015 in Dynamics AX 2012

Have you ever had a need to hide a few of your modules from all users/or a few users? Alternative you wanted to show only a few modules to all your users? To achieve this there are 2 options.

1. For basic/standard ax modules like AP, AR, budgeting etc you need to turn off/blank out the licensing code in the License information form (System administration->Setup->Licensing->License information) , feature sets tab.
2. For advanced features like Warehouse or for ISV provided solutions you can alternatively turn off the configuration key in the License configuration form (System administration->Setup->Licensing->License configuration)

For example you don’t think users in your company will use the Transportation management module. Since this an advanced feature you can take the second approach mentioned above. Go to licensing configuration and uncheck the configuration key Trade->Warehouse and Transportation management. Unfortunately there are a few side effects to this approach
– This will also turn off the advanced warehousing module
– This will in addition do a db sync and remove all Transportation management module related tables
– This feature will now be unavailable to all the users (not just a select few)

In fact both the options mentioned above have the exact same side effect. How can you get around this?

We will use the power of personalization to achieve the same effect. In AX you can decide what modules you want to see in the left nav and top address bar by using the navigation options. Here you can select/un-select the modules you want to see in the navigation. This personalization is company specific so the users can have different set of modules for different companies.

NavigationPaneOptions1

NavigationPaneOptions2

There is some coding involved but i will highlight all the code components. Let us take the example of hiding the transportation module. The idea is simple; On AX client start-up we will simulate the selection of navigation pane options and un-select the transportation module. Next we will persist this into personalization for the user. However when a user changes company they will see the module in the new company. To side step this we will also copy the personalization to other companies as well for the user. Another thing that can happen is that the user may decide to open up navigation pane options and select the module that we un-selected. To get around this we will modify the code in the Ok button to un-select it! Sneaky aren’t we!! Just remember the user will get frustrated and might complain that something is wrong or there is a bug, but hey it must have been a business decision in the first place to not grant access to the module so we should be fine.

So the code changes are as follows
– Intercept the initial AX client load in the class info, method startupPost.
– Intercept the Navigation Pane options dialog (Form SysNavPaneOptionsDialog, method OKButton: clicked)
And we are done!

Here is the code

Info class method startupPost
/*
No SYS code must exist in this method
*/
void startupPost()
{
    ModuleManager::HideTMSModule();
}

/* class ModuleManager */
/* 
1. This method hides the TMS module for all users except 3 users authorizeduser1, 
authorizeduser2, authorizeduser3. 
2. The way this does is by first retrieving all the modules by calling 
infolog.navPane().getButtons(). This returns a container with name of the module 
prefixed with 0 or 1. 1 if this module should be visible in navigation and 0 if 
it should be hidden.
3. Next the code loops through each of the buttons and looks for the name of 
the Transportation module main menu (TMS). If it finds that this module is 
enabled then it disables it if the current user is not one of the above 
listed users. You can use the same logic to hide/show this module based 
on a role the current user is assigned.
3. Once the new container is built with the modified values then the code 
persists the values back to personalization by calling 
infolog.navPane().setCurrMenuButtons() with the container as argument.
4. Finally it calls another method that copies this personalization to all 
other companies for this user - MPModuleManager::replaceNavOptionsInAllCompanies()
*/
public static void hideTMSModule()
{
    container   cont_navButtons, cont_navButtons_new;

    int i = 1;
    int j = 1;
    str aotValue;

    boolean isAdmin = false;

    #AOT
    #define.Zero('0')
    #define.MainMenuLocation('\\Menus\\MainMenu')

    if (curUserId() != 'authorizeduser1' && curUserId() != 'authorizeduser2' && curUserId() != 'authorizeduser3')
    {
        cont_navButtons = infolog.navPane().getButtons();
        cont_navButtons_new = conNull();
        j = 1;
        for(i=1; i<conLen(cont_navButtons); i++)
        {
            aotValue = conPeek(cont_navButtons, i);
            switch(aotValue)
            {
                case '1TMS':
                    aotValue = '0TMS';
                    break;
            }
            cont_navButtons_new = conIns(cont_navButtons_new, j, aotValue);
            j++;
        }
        infolog.navPane().setCurrMenuButtons(cont_navButtons_new);

        ModuleManager::replaceNavOptionsInAllCompanies();
    }
}

/*
This method copies over the navigation pane personalization option from the 
current company to all other companies, so that this module TMS is not 
available in all companies.

The code is based on standard Microsoft code in the method 
RetailSMB::SetRetailSMBAreaPagesForAllCompanies()
*/

public static void replaceNavOptionsInAllCompanies()
{
    #define.elementName('Usersetup')
    #define.designName('NavPaneOptionsButtons')

    DataArea        dataArea;
    SysLastValue    sysLastValue, sysLastValue2;

    select sysLastValue
    where
        sysLastValue.company == curext() &&
        sysLastValue.userId == curUserId() &&
        sysLastValue.elementName == #elementName &&
        sysLastValue.designName == #designName;

    if (sysLastValue)
    {
        while select id from dataArea
            where !dataArea.isVirtual && dataArea.Id != curext()
        {
            select forUpdate sysLastValue2
                where sysLastValue2.company == dataArea.Id &&
                    sysLastValue2.userId == curUserId() &&
                    sysLastValue2.elementName == #elementName &&
                    sysLastValue2.designName == #designName;

            ttsBegin;

            sysLastValue2.company = dataArea.Id;
            sysLastValue2.userId = curUserId();
            sysLastValue2.elementName = sysLastValue.elementName;
            sysLastValue2.recordType = sysLastValue.recordType;
            sysLastValue2.designName = sysLastValue.designName;
            sysLastValue2.isKernel = sysLastValue.isKernel;
            sysLastValue2.value = sysLastValue.value;

            sysLastValue2.write();

            ttsCommit;
        }
    }
}

/*
Form SysNavPaneOptionsDialog, method OKButton:clicked
*/
void clicked()
{
    container navButtons;
    int i, j;
    FormListItem item;
    str buttonInfo;
    ;

    for (i = 0 ; i < ListView.getCount();i++)
    {
        item = ListView.getItem(i);
        // walk through each element
        if (item.stateChecked())
        {
            buttonInfo = '1';
        }
        else
            buttonInfo = '0';

        if(item.data() == 'TMS')
        {
            if (curUserId() != 'authorizeduser1' && curUserId() != 'authorizeduser2' && curUserId() != 'authorizeduser3')
            {
                buttonInfo = '0';
            }
        }

        buttonInfo+= item.data();
        navButtons = conins(navButtons,i+1,buttonInfo);
    }

    for (j = 1; j <= conLen(hiddenMenuItems); j++)
    {
        buttonInfo = '0' + conPeek(hiddenMenuItems, j);
        navButtons = conins(navButtons,i+1,buttonInfo);
        i++;
    }

    infolog.navPane().setCurrMenuButtons(navButtons);

    if (RetailSMB::IsRetailSMBEnabled())
    {
        RetailSMB::SetRetailSMBAreaPagesForAllCompanies();
    }

    ModuleManager::replaceNavOptionsInAllCompanies();

    super();
}

There were a few useful resources that finally led me to this solution. Following are the related links.

http://www.mazikglobal.com/blog/hide-modules-role-ax-2012/
https://community.dynamics.com/ax/f/33/t/136277.aspx
There was another post by Mriganka Bhuyan but I am unable to locate the link.

A few other solutions that did not yield proper results
1. I tried setting a configuration key on the menu and dynamically disabling the configuration key based on the current user. For this I overrode the startupPost method of the Application class. I also tried modifying the setDefaultCompany of the Application class to achieve the same effect. However I noticed that when I call infolog.navPane().setCurrMenuButtons(), a recursive call is made back to setDefaultCompany of Application object. Which caused pure virtual function call error.
2. I also tried modifiying the SysDataAreaSelect form but that does not prevent a user from using the addressbar to change the company and so it had its own issues.
3. I tried setting the navigation pane options by using code to loop through all companies and setting the personalization, but that resulted in lot of infolog messages and unexpected behavior.

Leave a comment