Opening multiple projects in MorphX at startup

The MorphX IDE used for X++ programming in Microsoft Dynamics AX is a fairly decent environment to work in, but it definitely has some shortcomings and oddities. There is a “project” abstraction in MorphX that allows you to create a named group of objects that all relate to a project that you are working on. There’s little meaning to these projects, other than that. You can export all objects in a project into a single XPO file, but other than that, it’s basically just a structure to help a programmer keep track of a list of related objects. You can set a single project as your “startup project”, and that project will then open automatically when you open MorphX. Since I’m usually juggling three or four projects at a time, I’ve been thinking that it would be great if you could open a group of projects instead of just one, so I decided to try and write some code to do that.

My realization that this could be done at all mostly came from the book Microsoft Dynamics AX 2012 Development Cookbook. The section “Modifying the right-click context menu” described a method of setting and clearing the startup project via the context menu. Prior to reading this, I hadn’t realized that the development environment was as customizable as the AX front-end, though (in retrospect) it makes sense that it is.

The project I’m going to describe here will do a few, fairly simple, things:

  1. Maintain a list of startup projects in a simple text file, stored in my personal “documents” folder.
  2. Allow for adding and removing projects from this list, via the right-click context menu.
  3. Allow the user to open all projects in this list, via the MorphX “tools” menu.

I could probably do a lot more with this project, such as actually open the projects at MorphX startup, rather than just through the tools menu, but I’m fine with the project as-is, for now.

This blog post is going to walk through the steps necessary to implement this functionality.

First, we’re going to create a class called “AjhDevLaunchStartupProjects”. Our class will have methods to add and remove projects from the list, and to open all projects specified in the list. We’re not going to go overboard with efficiency here. I’m assuming that the list will, at most, have three or four projects on it at a time, so we’re simply going to read them from disk into an array, and write them back out from the array.

We’ll start with an entirely standard static constructor:

/andyhuey/5315492
1
2
3
4
public static AjhDevLaunchStartupProjects construct()
{
    return new AjhDevLaunchStartupProjects();
}

Now let’s add a method to get the file name for the text file:

/andyhuey/5315509
1
2
3
4
5
6
7
8
9
private str getStartProjFileName()
{
    str myDocsPath;
 
    #WinAPI
 
    myDocsPath = WinAPI::getFolderPath(#CSIDL_PERSONAL);
    return myDocsPath + @"\axStartProjects.txt";
}

Now, methods to read and write the file:

/andyhuey/5315524
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
private Array getProjectList()
{
    // get the project list from a file.
    str startProjFileName;
    TextBuffer tbProjList;
    Array projects = new Array(Types::String);
    int nProjects;
 
    startProjFileName = this.getStartProjFileName();
    tbProjList = new TextBuffer();
 
    // if it doesn't exist, create an empty file & return.
    if (!WinAPI::fileExists(startProjFileName))
    {
        WinAPI::createFile(startProjFileName);
        return projects;
    }
 
    // should probably assert permission...
    tbProjList.fromFile(startProjFileName);
 
    nProjects = 0;
    while (tbProjList.nextToken(true))
    {
        nProjects++;
        projects.value(nProjects, tbProjList.token());
    }
    return projects;
}
 
private boolean writeProjectList(Array projects)
{
    // write out a project list to file, overwriting existing list.
    str startProjFileName;
    TextBuffer tbProjList;
    int i;
 
    startProjFileName = this.getStartProjFileName();
 
    tbProjList = new TextBuffer();
 
    for (i=1; i <= projects.lastIndex(); i++)
    {
        tbProjList.appendText(projects.value(i));
        tbProjList.appendText("\n");
    }
 
    return tbProjList.toFile(startProjFileName);
}

And the methods to add and remove projects from the list:

/andyhuey/5315547
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
private void addProject(str newProject)
{
    // add a new project to the list.
    Array projects;
    int i;
 
    projects = this.getProjectList();
 
    // make sure it's not already there...
    for (i=1; i <= projects.lastIndex(); i++)
    {
        if (projects.value(i) == newProject)
            return;
    }
    // add it and save.
    projects.value(projects.lastIndex()+1, newProject);
    this.writeProjectList(projects);
}
 
private void removeProject(str projectToRemove)
{
    // remove a project from the list.
    Array projectsIn, projectsOut;
    int i, j;
 
    projectsIn = this.getProjectList();
    projectsOut = new Array(Types::String);
 
    j=1;
    for (i=1; i <= projectsIn.lastIndex(); i++)
    {
        if (projectsIn.value(i) != projectToRemove)
        {
            projectsOut.value(j, projectsIn.value(i));
            j++;
        }
    }
    this.writeProjectList(projectsOut);
}

And here’s the code to open all active projects:

/andyhuey/5315552
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
public void openAllProjects()
{
    // get the project list, and open all projects.
    Array projects;
    int i;
    ProjectNode sharedProjects, privateProjects, projectNode;
 
    projects = this.getProjectList();
 
    sharedProjects = Infolog.projectRootNode().AOTfindChild('Shared');
    if (!sharedProjects)
        throw error("Error: cannot locate shared project node!");
 
    privateProjects = Infolog.projectRootNode().AOTfindChild('Private');
    if (!privateProjects)
        throw error("Error: cannot locate private project node!");
 
    for (i=1; i <= projects.lastIndex(); i++)
    {
        projectNode = sharedProjects.AOTfindChild(projects.value(i));
        if (!ProjectNode)
            projectNode = privateProjects.AOTfindChild(projects.value(i));
 
        if (projectNode)
            projectNode.getRunNode();
        else
            warning(strFmt("Project %1 cannot be found.", projects.value(i)));
    }
}

(I probably found the code to open a project from this blog post, or a similar one.)
Now, we will create three new “action” menu items:

  1. AjhDevStartupProjectAdd – to add a project.
  2. AjhDevStartupProjectRemove – to remove a project.
  3. AjhDevStartupProjectOpenAll – to open all projects.

Each one will have ObjectType set to “Class”, and the object will be our class, “AjhDevLaunchStartupProjects”. The static main() method in our class will use args.menuItemName() to determine which action to take.
Here’s that method:

/andyhuey/5315559
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
public static void main(Args args)
{
    AjhDevLaunchStartupProjects obj = AjhDevLaunchStartupProjects::construct();
    SysContextMenu contextMenu;
    str projectName, x;
    ;
 
    // should always be called from a menu item.
    if (!args.menuItemName())
    {
        return;
    }
 
    // if called from the add-ins context menu...
    if (SysContextMenu::startedFrom(args))
    {
        contextMenu = args.parmObject();
        projectName = contextMenu.getFirstNode().treeNodeName();
    }
 
    switch (args.menuItemName())
    {
        case menuitemActionStr(AjhDevStartupProjectAdd):
            obj.addProject(projectName);
            break;
        case menuitemActionStr(AjhDevStartupProjectRemove):
            obj.removeProject(projectName);
            break;
        case menuitemActionStr(AjhDevStartupProjectOpenAll):
            obj.openAllProjects();
        default:
            return;
    }
}

The add and remove menu items will be added to the SysContextMenu. This way, they will show in the context menu under “add-ins”. The “open all” menu item will be added to the DevelopmentTools menu. This way, it will show under the “Tools” menu in MorphX.

We will also change the “verifyItem” method of the “SysContextMenu” class, so that the add & remove items will only show in the context menu for projects (and not for other objects). (If we wanted to go further with this, we would also add logic here to show only one or the other option, depending on whether or not the project is already on the startup list.)

Here’s the code that we will add to “verifyItem”, at the end of the large case statement there:

/andyhuey/5315566
1
2
3
4
5
6
7
8
    // ajh 2013-04-03: my own startup project thing...
    case menuitemActionStr(AjhDevStartupProjectAdd):
    case menuitemActionStr(AjhDevStartupProjectRemove):
        if (firstNode.handle() != classNum(ProjectNode) || !match(#pathProjects, firstNode.treeNodePath()))
        {
            return 0;
        }
        return 1;

So I think this is all pretty straightforward. My purpose in writing this up in such detail was largely so that I could get a bit of X++ code on this blog, and in the hope that someone else might find this useful. I haven’t had the chance to write much general-purpose code at my current job, so there isn’t much I’m working on that would be appropriate to post here, but this seemed like a worthwhile little project to work on, and hopefully it may prove helpful to someone else.

Leave a Reply