# Multi Node Workspace Example

**URL:** <https://forum.inductiveautomation.com/t/multi-node-workspace-example/62705>\
**Category:** Module Development\
**Tags:** perspective\
**Created:** [August 4, 2022, 11:44pm UTC](https://forum.inductiveautomation.com/t/multi-node-workspace-example/62705 "2022-08-04T23:44:33Z")\
**Posts on this page:** 20\
**Page:** 1

<div class="post-metadata">

**Author:** ![kgamble](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/kgamble/32/8019_2.png) [@kgamble](https://forum.inductiveautomation.com/u/kgamble)\
**Post date:** [August 4, 2022, 11:44pm UTC](https://forum.inductiveautomation.com/t/multi-node-workspace-example/62705/1 "2022-08-04T23:44:33Z")

</div>

Are there any public examples of modules that implement multiple Nav tree nodes for their designer workspace?

i.e. Perspective has the Style workspace and the View workspace  
 ![image](https://us1.discourse-cdn.com/inductiveautomation/original/3X/b/9/b9310afb03707b158fe5c66954f32f0528b75772.png)

I have been using the `TabbedResourceWorkspace`, but that handles all of the Project resource nav tree work for me, and seems to be restricting the ability to customize the root node of the workspace.

I started digging around in the other workspace types, and playing with `context.getProjectBrowserRoot()#addChild()`, but when I register my `TabbedResourceWorkspace` its automatically adding the project browser node, and then I end up with a duplicate node.

I am mostly working off this 10 year old comment, so who knows if this has been updated and I am on the wrong path.

> [@ignition.designer.model.DesignerContext](https://forum.inductiveautomation.com/t/ignition-designer-model-designercontext/6206/2):
>
> 1. Create your workspace implementation, and register it using context.registerResourceWorkspace()
> 2. Make your node by creating a subclass of AbstractNavTreeNode. Make sure to override getWorkspaceName() to return the name of your workspace (this is what will automatically switch to your workspace when the node is selected)
> 3. Add your node using context.getProjectBrowserRoot().addChild()

If anyone knows of any examples that would be great, or just a few comments on how to get headed in the right direction.

---

<div class="post-metadata">

**Author:** ![pturmel](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/pturmel/32/3295_2.png) [@pturmel](https://forum.inductiveautomation.com/u/pturmel)\
**Post date:** [August 5, 2022, 1:00am UTC](https://forum.inductiveautomation.com/t/multi-node-workspace-example/62705/2 "2022-08-05T01:00:32Z")

</div>

My only foray into this area implements `ResourceWorkspace` subclassing `JideTabbedPane`. The load in the designer startup hook looks like this:

```auto
        EnipWorkspace workspace = new EnipWorkspace(context);
        context.registerResourceWorkspace(workspace);
        backplane = new EnipBackplaneNode(workspace, context);
        context.getProjectBrowserRoot().addChild(backplane);

```

You might inspect the `TabbedResourceWorkspace` to figure out which method is adding your node and override that part.

I don’t remember all the details, but this was a slog.

---

<div class="post-metadata">

**Author:** ![bmusson](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/bmusson/32/87530_2.png) [@bmusson](https://forum.inductiveautomation.com/u/bmusson)\
**Post date:** [August 5, 2022, 3:11am UTC](https://forum.inductiveautomation.com/t/multi-node-workspace-example/62705/3 "2022-08-05T03:11:17Z")

</div>

I’ve done this, but in a project that I can’t directly share.

Basically it involves creating your own root node and making your resource workspaces use that root node by overriding `getNavTreeNodeParent`.

I’ll see if I can put together a short example.

---

<div class="post-metadata">

**Author:** ![kgamble](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/kgamble/32/8019_2.png) [@kgamble](https://forum.inductiveautomation.com/u/kgamble)\
**Post date:** [August 5, 2022, 4:27pm UTC](https://forum.inductiveautomation.com/t/multi-node-workspace-example/62705/4 "2022-08-05T16:27:35Z")

</div>

> [@pturmel](#):
>
> I don’t remember all the details, but this was a slog.

This is definitely an effort... lol

I am slowly making my way through this, but essentially I started with what @bmusson and so far this is what I have:

1. I created an implementation of the `AbstractNavTreeNode` with no children, this will be the parent node
2. I then created two classes (one for each node I want) that extend `TabbedResourceWorkspace`
3. In my constructor for the workspaces, I made the parent node a constructor arg, and then override `getNavTreeNodeParent` to return that provided parent node
4. I register my parent node in the designer hook with `this.context.getProjectBrowserRoot()#addChild`
5. I then register each workspace with `this.context#registerResourceWorkspace`
6. At this point, I have a node element with two child nodes. Progress! I will now refer to these as the A node and the B node
7. I created a class that extends `WorkspaceWelcomePanel` in a way that is abstract between my two nodes, so that both the A and B nodes can share the same panel, though they each create their own.
8. That's as far as I have gotten... now I am in "which class Is initializing in what order, and where does this mysterious NPE come from" purgatory...

Will update when I have more useful steps.

EDIT:

> **The NPE for prosperities sake:**
>
> ```auto
> Exception in thread "AWT-EventQueue-0" java.lang.NullPointerException
> at com.inductiveautomation.ignition.designer.tabbedworkspace.TabbedResourceWorkspace.registerNavTreeNode(TabbedResourceWorkspace.java:206)
> at com.inductiveautomation.ignition.designer.tabbedworkspace.TabbedResourceWorkspace.lambda$new$0(TabbedResourceWorkspace.java:125)
> at java.desktop/java.awt.event.InvocationEvent.dispatch(Unknown Source)
> at java.desktop/java.awt.EventQueue.dispatchEventImpl(Unknown Source)
> at java.desktop/java.awt.EventQueue$4.run(Unknown Source)
> at java.desktop/java.awt.EventQueue$4.run(Unknown Source)
> at java.base/java.security.AccessController.doPrivileged(Native Method)
> at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(Unknown Source)
> at java.desktop/java.awt.EventQueue.dispatchEvent(Unknown Source)
> at java.desktop/java.awt.EventDispatchThread.pumpOneEventForFilters(Unknown Source)
> at java.desktop/java.awt.EventDispatchThread.pumpEventsForFilter(Unknown Source)
> at java.desktop/java.awt.EventDispatchThread.pumpEventsForHierarchy(Unknown Source)
> at java.desktop/java.awt.EventDispatchThread.pumpEvents(Unknown Source)
> at java.desktop/java.awt.EventDispatchThread.pumpEvents(Unknown Source)
> at java.desktop/java.awt.EventDispatchThread.run(Unknown Source)
> Exception in thread "AWT-EventQueue-0" java.lang.NullPointerException
> at com.inductiveautomation.ignition.designer.tabbedworkspace.TabbedResourceWorkspace.registerNavTreeNode(TabbedResourceWorkspace.java:206)
> at com.inductiveautomation.ignition.designer.tabbedworkspace.TabbedResourceWorkspace.lambda$new$0(TabbedResourceWorkspace.java:125)
> at java.desktop/java.awt.event.InvocationEvent.dispatch(Unknown Source)
> at java.desktop/java.awt.EventQueue.dispatchEventImpl(Unknown Source)
> at java.desktop/java.awt.EventQueue$4.run(Unknown Source)
> at java.desktop/java.awt.EventQueue$4.run(Unknown Source)
> at java.base/java.security.AccessController.doPrivileged(Native Method)
> at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(Unknown Source)
> at java.desktop/java.awt.EventQueue.dispatchEvent(Unknown Source)
> at java.desktop/java.awt.EventDispatchThread.pumpOneEventForFilters(Unknown Source)
> at java.desktop/java.awt.EventDispatchThread.pumpEventsForFilter(Unknown Source)
> at java.desktop/java.awt.EventDispatchThread.pumpEventsForHierarchy(Unknown Source)
> at java.desktop/java.awt.EventDispatchThread.pumpEvents(Unknown Source)
> at java.desktop/java.awt.EventDispatchThread.pumpEvents(Unknown Source)
> at java.desktop/java.awt.EventDispatchThread.run(Unknown Source)
> 
> ```

---

<div class="post-metadata">

**Author:** ![pturmel](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/pturmel/32/3295_2.png) [@pturmel](https://forum.inductiveautomation.com/u/pturmel)\
**Post date:** [August 5, 2022, 4:40pm UTC](https://forum.inductiveautomation.com/t/multi-node-workspace-example/62705/5 "2022-08-05T16:40:53Z")

</div>

Should only need one workspace. Then two types **tabs** within them.

---

<div class="post-metadata">

**Author:** ![kgamble](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/kgamble/32/8019_2.png) [@kgamble](https://forum.inductiveautomation.com/u/kgamble)\
**Post date:** [August 5, 2022, 5:34pm UTC](https://forum.inductiveautomation.com/t/multi-node-workspace-example/62705/6 "2022-08-05T17:34:37Z")

</div>

I thought the tabs were for having multiple resources of the same type open? I.e multiple scripts in the project scripts.

I am going to have two different types of resources, one that’s gateway scoped and one that’s project scoped

---

<div class="post-metadata">

**Author:** ![paul-griffith](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/paul-griffith/32/37249_2.png) [@paul-griffith](https://forum.inductiveautomation.com/u/paul-griffith)\
**Post date:** [August 5, 2022, 5:45pm UTC](https://forum.inductiveautomation.com/t/multi-node-workspace-example/62705/7 "2022-08-05T17:45:46Z")

</div>

> [@kgamble](#):
>
> I created an implementation of the `AbstractNavTreeNode` with no children, this will be the parent node

This should extend `MutableNavTreeNode`, so you can manually add your child nodes.

> [@kgamble](#):
>
> At this point, I have a node element with two child nodes. Progress! I will now refer to these as the A node and the B node

Are you manually adding these nodes as children of your root node? Ensure you attach them to the parent _after_ you've added the root node to the project browser root.

---

<div class="post-metadata">

**Author:** ![bmusson](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/bmusson/32/87530_2.png) [@bmusson](https://forum.inductiveautomation.com/u/bmusson)\
**Post date:** [August 5, 2022, 7:43pm UTC](https://forum.inductiveautomation.com/t/multi-node-workspace-example/62705/8 "2022-08-05T19:43:07Z")

</div>

> [@pturmel](#):
>
> Should only need one workspace. Then two types **tabs** within them.

I'm interested in how to accomplish this. My current implementation has workspaces for each resource, but some of them would definitely be better as tabs of a single workspace.

Taking a shot in the dark - does this involve `AbstractNavTreeNode#getWorkspaceName()` and `ResourceWorkspace#getKey()`?

---

<div class="post-metadata">

**Author:** ![kgamble](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/kgamble/32/8019_2.png) [@kgamble](https://forum.inductiveautomation.com/u/kgamble)\
**Post date:** [August 5, 2022, 8:11pm UTC](https://forum.inductiveautomation.com/t/multi-node-workspace-example/62705/9 "2022-08-05T20:11:11Z")

</div>

> [@paul-griffith](#):
>
> Are you manually adding these nodes as children of your root node? Ensure you attach them to the parent _after_ you’ve added the root node to the project browser root.

I guess maybe the question is, am I attaching these incorrectly, because I feel like it's the right order?

Currently in my designer startup I am doing this:

```java
// Intialize the root navigation node in the project browser
ConfigManagerNavTreeNode configManagerNavTreeNode = new ConfigManagerNavTreeNode();
this.context.getProjectBrowserRoot().addChild(configManagerNavTreeNode);

// Create and register my workspaces
ProjectConfigWorkspace projectWorkspace = new ProjectConfigWorkspace(this.context, configManagerNavTreeNode);
GatewayConfigWorkspace gatewayWorkspace = new GatewayConfigWorkspace(this.context, configManagerNavTreeNode);
this.context.registerResourceWorkspace(projectWorkspace);
this.context.registerResourceWorkspace(gatewayWorkspace);

// Add my workspaces to the top level navTree
configManagerNavTreeNode.addChild(gatewayWorkspace.getRootNode());
configManagerNavTreeNode.addChild(projectWorkspace.getRootNode());

```

Then in the constructor for my workspaces, I am doing

```java
public GatewayConfigWorkspace(DesignerContext context, AbstractNavTreeNode navTreeNode) {
    super(context, ResourceDescriptor.builder()
            .resourceType(ConfigResource.RESOURCE_TYPE)
            .nounKey("ConfigManager.config.noun")
            
            .rootFolderText("Gateway")
            .rootIcon(VectorIcons.get("gateway"))
            .navTreeLocation(999)

            .build(), navTreeNode);
}

```

With `this.rootNode` being what's returned in the `getNavTreeNodeParent` method, and set in the `AbstractConfigWorkspace` constructor

```java
protected AbstractConfigWorkspace(DesignerContext context, ResourceDescriptor resourceDescriptor, AbstractNavTreeNode rootNode) {
    super(context, resourceDescriptor);
    this.rootNode = rootNode;
}

@Override
public MutableNavTreeNode getNavTreeNodeParent() {
    return (MutableNavTreeNode) this.rootNode;
}

```

---

<div class="post-metadata">

**Author:** ![paul-griffith](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/paul-griffith/32/37249_2.png) [@paul-griffith](https://forum.inductiveautomation.com/u/paul-griffith)\
**Post date:** [August 5, 2022, 8:19pm UTC](https://forum.inductiveautomation.com/t/multi-node-workspace-example/62705/10 "2022-08-05T20:19:33Z")

</div>

That seems correct, at a glance. Are you still getting the same NPE?

---

<div class="post-metadata">

**Author:** ![kgamble](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/kgamble/32/8019_2.png) [@kgamble](https://forum.inductiveautomation.com/u/kgamble)\
**Post date:** [August 5, 2022, 8:27pm UTC](https://forum.inductiveautomation.com/t/multi-node-workspace-example/62705/11 "2022-08-05T20:27:02Z")

</div>

This is the one that just showed up when I started the designer the last time

```auto
13:08:10.525 [Designer-Startup] INFO designer.main - Starting module: Process Config Manager [+6695]
Exception in thread "AWT-EventQueue-0" java.lang.NullPointerException
	at com.inductiveautomation.ignition.designer.navtree.model.MutableNavTreeNode.add(MutableNavTreeNode.java:37)
	at com.inductiveautomation.ignition.designer.navtree.model.MutableNavTreeNode.lambda$addChild$0(MutableNavTreeNode.java:25)
	at java.desktop/java.awt.event.InvocationEvent.dispatch(Unknown Source)
	at java.desktop/java.awt.EventQueue.dispatchEventImpl(Unknown Source)
	at java.desktop/java.awt.EventQueue$4.run(Unknown Source)
	at java.desktop/java.awt.EventQueue$4.run(Unknown Source)
	at java.base/java.security.AccessController.doPrivileged(Native Method)
	at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(Unknown Source)
	at java.desktop/java.awt.EventQueue.dispatchEvent(Unknown Source)
	at java.desktop/java.awt.EventDispatchThread.pumpOneEventForFilters(Unknown Source)
	at java.desktop/java.awt.EventDispatchThread.pumpEventsForFilter(Unknown Source)
	at java.desktop/java.awt.EventDispatchThread.pumpEventsForHierarchy(Unknown Source)
	at java.desktop/java.awt.EventDispatchThread.pumpEvents(Unknown Source)
	at java.desktop/java.awt.EventDispatchThread.pumpEvents(Unknown Source)
	at java.desktop/java.awt.EventDispatchThread.run(Unknown Source)
Exception in thread "AWT-EventQueue-0" java.lang.NullPointerException
	at com.inductiveautomation.ignition.designer.navtree.model.MutableNavTreeNode.add(MutableNavTreeNode.java:37)
	at com.inductiveautomation.ignition.designer.navtree.model.MutableNavTreeNode.lambda$addChild$0(MutableNavTreeNode.java:25)
	at java.desktop/java.awt.event.InvocationEvent.dispatch(Unknown Source)
	at java.desktop/java.awt.EventQueue.dispatchEventImpl(Unknown Source)
	at java.desktop/java.awt.EventQueue$4.run(Unknown Source)
	at java.desktop/java.awt.EventQueue$4.run(Unknown Source)
	at java.base/java.security.AccessController.doPrivileged(Native Method)
	at java.base/java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(Unknown Source)
	at java.desktop/java.awt.EventQueue.dispatchEvent(Unknown Source)
	at java.desktop/java.awt.EventDispatchThread.pumpOneEventForFilters(Unknown Source)
	at java.desktop/java.awt.EventDispatchThread.pumpEventsForFilter(Unknown Source)
	at java.desktop/java.awt.EventDispatchThread.pumpEventsForHierarchy(Unknown Source)
	at java.desktop/java.awt.EventDispatchThread.pumpEvents(Unknown Source)
	at java.desktop/java.awt.EventDispatchThread.pumpEvents(Unknown Source)
	at java.desktop/java.awt.EventDispatchThread.run(Unknown Source)
13:08:10.538 [Designer-Startup] INFO designer.main - Initializing Scripting... [+6708]

```

This also happens when the designer hook starts up, before it starts initializing scripting.

EDIT: This looks like its a different NPE, however I am still not sure what’s causing it since I don’t see my package anywhere in this stack trace

---

<div class="post-metadata">

**Author:** ![paul-griffith](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/paul-griffith/32/37249_2.png) [@paul-griffith](https://forum.inductiveautomation.com/u/paul-griffith)\
**Post date:** [August 5, 2022, 8:55pm UTC](https://forum.inductiveautomation.com/t/multi-node-workspace-example/62705/12 "2022-08-05T20:55:43Z")

</div>

Look for a call to `com.inductiveautomation.ignition.designer.navtree.model.MutableNavTreeNode#addChild`; you’re providing a null (or not-yet-constructed?) `node` to `addChild`, which is the cause of that NPE.

---

<div class="post-metadata">

**Author:** ![paul-griffith](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/paul-griffith/32/37249_2.png) [@paul-griffith](https://forum.inductiveautomation.com/u/paul-griffith)\
**Post date:** [August 5, 2022, 8:57pm UTC](https://forum.inductiveautomation.com/t/multi-node-workspace-example/62705/13 "2022-08-05T20:57:05Z")

</div>

Wait, this isn’t right:

```java
// Intialize the root navigation node in the project browser
ConfigManagerNavTreeNode configManagerNavTreeNode = new ConfigManagerNavTreeNode();
this.context.getProjectBrowserRoot().addChild(configManagerNavTreeNode);

// Create and register my workspaces
ProjectConfigWorkspace projectWorkspace = new ProjectConfigWorkspace(this.context, configManagerNavTreeNode);
GatewayConfigWorkspace gatewayWorkspace = new GatewayConfigWorkspace(this.context, configManagerNavTreeNode);
this.context.registerResourceWorkspace(projectWorkspace);
this.context.registerResourceWorkspace(gatewayWorkspace);

// Add my workspaces to the top level navTree
configManagerNavTreeNode.addChild(gatewayWorkspace.getRootNode());
configManagerNavTreeNode.addChild(projectWorkspace.getRootNode());

```

Isn’t this:  
`configManagerNavTreeNode.addChild(gatewayWorkspace.getRootNode());`  
Trying to add `configManagerNavTreeNode` to itself, as a child?

---

<div class="post-metadata">

**Author:** ![bmusson](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/bmusson/32/87530_2.png) [@bmusson](https://forum.inductiveautomation.com/u/bmusson)\
**Post date:** [August 5, 2022, 9:07pm UTC](https://forum.inductiveautomation.com/t/multi-node-workspace-example/62705/14 "2022-08-05T21:07:55Z")

</div>

I don’t think you need

```auto
configManagerNavTreeNode.addChild(gatewayWorkspace.getRootNode());
configManagerNavTreeNode.addChild(projectWorkspace.getRootNode());

```

at all since you’re overriding `getNavTreeNodeParent`?

---

<div class="post-metadata">

**Author:** ![kgamble](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/kgamble/32/8019_2.png) [@kgamble](https://forum.inductiveautomation.com/u/kgamble)\
**Post date:** [August 5, 2022, 10:22pm UTC](https://forum.inductiveautomation.com/t/multi-node-workspace-example/62705/15 "2022-08-05T22:22:26Z")

</div>

Y’all are right, that was the problem!

![image](https://us1.discourse-cdn.com/inductiveautomation/original/3X/1/7/172339f923ed9431bd324fc6c00817c038e57610.png)

I hope this thread makes this easier for anyone trying to attempt this in the future, as in the wise words of @pturmel this was “a slog”…

---

<div class="post-metadata">

**Author:** ![pturmel](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/pturmel/32/3295_2.png) [@pturmel](https://forum.inductiveautomation.com/u/pturmel)\
**Post date:** [August 5, 2022, 10:29pm UTC](https://forum.inductiveautomation.com/t/multi-node-workspace-example/62705/16 "2022-08-05T22:29:41Z")

</div>

> [@bmusson](#):
>
> I’m interested in how to accomplish this.

I've got too much on my plate for the level of effort it would require to get back up to speed on that code (cringe). In a week or two, perhaps.

---

<div class="post-metadata">

**Author:** ![paul-griffith](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/paul-griffith/32/37249_2.png) [@paul-griffith](https://forum.inductiveautomation.com/u/paul-griffith)\
**Post date:** [August 5, 2022, 10:40pm UTC](https://forum.inductiveautomation.com/t/multi-node-workspace-example/62705/17 "2022-08-05T22:40:37Z")

</div>

Again, at a glance, but I don’t think it would be _too_ hard to extend TabbedResourceWorkspace to support different editors. You would just have to conditionally return the appropriate editor from `newResourceEditor(ResourcePath)`.

---

<div class="post-metadata">

**Author:** ![kgamble](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/kgamble/32/8019_2.png) [@kgamble](https://forum.inductiveautomation.com/u/kgamble)\
**Post date:** [August 5, 2022, 11:31pm UTC](https://forum.inductiveautomation.com/t/multi-node-workspace-example/62705/18 "2022-08-05T23:31:03Z")

</div>

As I have gotten a little further, I am realizing I don't see an obvious construct for "Gateway scoped resources" just "Project scoped resources". Before I go down the rabbit hole of managing my own resource folders on the gateway, is there already something in place for this?

#### i.e. Perspective themes:

```python
-data
-----modules
------------com.inductiveautomation.perspective
-----------------------------------------------themes
-----------------------------------------------icons
-----------------------------------------------fonts
------projects
--------------ProjectName
-------------------------com.inductiveautomation.perspective
-------------------------------------------------------------views
-------------------------------------------------------------styles

```

EDIT: Probably not the right thread for that question, but I've created a lot of new threads recently, so figured I'd save the space... lol

---

<div class="post-metadata">

**Author:** ![bmusson](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/bmusson/32/87530_2.png) [@bmusson](https://forum.inductiveautomation.com/u/bmusson)\
**Post date:** [August 6, 2022, 12:33am UTC](https://forum.inductiveautomation.com/t/multi-node-workspace-example/62705/19 "2022-08-06T00:33:10Z")

</div>

I think you’re on your own unless you want to add Perspective as a dependency to use its asset manager concept.

[https://files.inductiveautomation.com/sdk/javadoc/ignition81/8.1.18/com/inductiveautomation/perspective/gateway/assets/AbstractAssetManager.html](https://files.inductiveautomation.com/sdk/javadoc/ignition81/8.1.18/com/inductiveautomation/perspective/gateway/assets/AbstractAssetManager.html)

---

<div class="post-metadata">

**Author:** ![rettdavidp](https://sea1.discourse-cdn.com/inductiveautomation/user_avatar/forum.inductiveautomation.com/rettdavidp/32/82281_2.png) [@rettdavidp](https://forum.inductiveautomation.com/u/rettdavidp)\
**Post date:** [December 20, 2025, 1:17am UTC](https://forum.inductiveautomation.com/t/multi-node-workspace-example/62705/20 "2025-12-20T01:17:09Z")

</div>

Not to necro an old thread, but here I am in 2025 facing the same problem and I wanted to first thank everyone for the discussion it was very helpful and I wanted to add some details that might be useful to others. I spent around a week in my free time trying to get this working before finding this thread, it was indeed "a slog."

@kgamble's use case in this scenario specifically desired two separate Workspace's, but as @pturmel more or less pointed out - the Vision & Perspective modules only use one workspace. You can test this by just clicking on every `ResourceType` (Like Styles, Views) inside the Perspective folder and you'll notice nothing changes with regards to your Perspective Workspace. Each tab in the `TabbedResourceWorkspace` can be a different `ResourceType` and this is because of overriding `newResourceEditor` like @paul-griffith mentioned. On top of this, notice that the root "Perspective" node has no right click actions and shares the same Workspace as Views/Styles when you click it.

As far as I can tell, multi-node modules like Perspective are structured similar to the following:

1. Define a custom `MutableNavTreeNode` to act as the parent node, make sure to `@Override` `getWorkspaceName()` otherwise clicking on the parent node won't switch the `Workspace` you might be clicking from.

```java
public class CustomNavTreeNode extends MutableNavTreeNode {
    Icon ICON = DesignerHook.RESOURCE_ICON;
    String NAME = "Custom Multi Node Resource";
    String TEXT = "Custom Multi Node Resource";

    protected CustomNavTreeNode() {
        super(true);
    }

    @Override
    public Icon getIcon() {
        return ICON;
    }

    @Override
    public String getName() {
        return NAME;
    }

    @Override
    public String getText() {
        return TEXT;
    }

    @Override
    public String getWorkspaceName() {
        return DesignerHook.WORKSPACE_NAME;
    }

    @Override
    public void showPopupMenu(Component source, int x, int y, int modifiers, TreePath[] paths, List<AbstractNavTreeNode> selection) {
        // Intentionally do nothing to suppress the right-click popup menu
    }
}

```

1. Register your custom `MutableNavTreeNode` in the `DesignerHook`

```java
public void startup(DesignerContext context, LicenseState activationState) throws Exception {
        this.context = context;

        CustomNavTreeNode rootFolder = new CustomNavTreeNode();

        context.getProjectBrowserRoot().addChild(rootFolder);
       
       ...
}

```

1. In your custom Workspace, add a constructor argument passing in your custom `MutableNavTreeNode` as an argument. @kgamble's method of mapping to `this.rootNode` did not work for me. I received a `ResourceFolderNode` class casting error on Designer startup. Mapping to a custom private variable fixed it.

```java
public class CustomResourceWorkspace extends TabbedResourceWorkspace {
    public static final ResourceDescriptor DESCRIPTOR = ResourceDescriptor.builder()
            .resourceType(CustomResource.RESOURCE_TYPE)
            .nounKey("custom.noun")
            .rootFolderText("Custom Nested Resource 1")
            .rootIcon(DesignerHook.NESTED_RESOUCE_1_ICON)
            .build();

    private final CustomNavTreeNode navTreeParentNode;

    public CustomResourceWorkspace(DesignerContext context, CustomNavTreeNode navTreeParentNode) {
        super(context, DESCRIPTOR);
        this.navTreeParentNode = navTreeParentNode;
    }
   ...

```

1. Create your `Workspace` in the `DesignerHook` and register it with the context

```java
public void startup(DesignerContext context, LicenseState activationState) throws Exception {
        this.context = context;

        CustomNavTreeNode rootFolder = new CustomNavTreeNode();

        context.getProjectBrowserRoot().addChild(rootFolder);
       
        CustomResourceWorkspace workspace = new CustomResourceWorkspace(context, rootFolder);

        context.registerResourceWorkspace(workspace);
      ...
}

```

There ya go! You now have a parent node in the `NavTree` and a nested resource similar to Perspective's `Views`. To add additional nodes, you have to make custom `ResourceFolderNode`'s and pass your `CustomResourceWorkspace` into the `ResourceFolderNode`'s constructor (At least I think that's how you do it.)

The main reason this whole process is "a slog" is because (in my opinion) `context.registerResourceWorkspace(workspace)` creates a `ResourceFolderNode` for you without the javadocs really telling you this. Adding a note about this to the javadocs would make it more clear to future newbie module developers (like myself) what actually happens when you call `registerResourceWorkspace`.

[Next page](https://forum.inductiveautomation.com/t/multi-node-workspace-example/62705.md?page=2)
