Creating a button component that triggers a navigation event

Hi all,

I am attempting to make a simple button with pre-implemented navigation code in a module I am creating. But I cannot figure out how to get this scripting into the Java Module.

Basically, the component has 2x String Properties - Window Path and Tag Path. And will use these to open the popup at the vision location of Window Path and feed in the Tag Path as a parameter to the window. For example, a Jython Script for the same task on a button with 2 parameters "WindowPath" and "TagPath":

DisplayHandle = system.gui.getCurrentDesktop()
DesktopNav = system.nav.desktop(DisplayHandle)

DesktopNav.openWindowInstance(event.source.WindowPath, {"TagPath" : event.source.TagPath})

I tried looking at documentation but just could not wrap my head around how to do it in Java. If anyone can offer and help I would be very appreciative.

That is a really bad idea. And it is an internal implementation detail of the Vision module, so I doubt there's any way to get to it.

Leave human tasks (connecting components, binding values, arranging nav) to the human using the designer.

Me and a colleague have been looking at this for a hour or two and have found that we can get this to work using:

NavUtilitiesDispatcher navUtilsDisp = new NavUtilitiesDispatcher(FPMIApp.getInstance());

PyDictionary parameters= new PyDictionary();
parameters.put("TagRoot", tagRoot);
	
navUtilsDisp.openWindowInstance(windowPath, parameters);
navUtilsDisp.centerWindow(this.windowPath);

And it seems to work perfectly well. However, are you saying that this has the change to break general parts of the vision module? Do Note: I am aware that the FPMIApp.getInstance() is depreciated but I currently see no alternatives.

Let me know what we are all thinking and if it has the potential to cause major issues I will just go with the trusty internal Jython Scripting on the component, but for the application an internal method would be perfect.

My recommendation is to not do any navigation from within your component. Full stop.

Make sure your component properly supports ordinary event scripting for your designers to use.

It's less about access to internals (though that does have some fundamental risks on its own) and more about a fundamental design paradigm. Throughout Vision and Perspective, components provide hooks, and the developer working in the Designer chooses to implement (or not) whatever desired functionality they want in those hooks. By "pre-scripting" functionality in your components, you're taking that flexibility away from your end users.

You could probably achieve something close to the desired result without any "unsafe" edges by creating a custom component with some scripts automatically placed on it - the default behavior on drop is to do what you want, but end users are still able to change or disable this behavior if they want to.

This is also not to say that it's not potentially okay to offer a "magic button" via a custom module - it's your module, and of course you're free to do whatever you want with it, and you know your intended audience better than us. But if it's intended for wider distribution than "people you can directly explain this to", I'd highly recommend sticking to exposing hooks over exposing functionality if you're creating something as fundamental as a button.

3 Likes

Would you be able to elaborate or send some links to resources on how to do this? But thankyou for your insight it is really helpful!

This was a bit of a voyage of discovery for me, so bear with me if any of these explanations are a bit half-baked.

So, it looks like you want your component class to implement com.inductiveautomation.vision.api.client.components.model.SelfBinder - as the name/Javadoc suggest, it's used for components that set up their own event scripts (like the Paintable Canvas).

The first thing you want to do in your implementation of installBindings is check that no bindings already exist on the component - by definition, since your component is a SelfBinder, that will only be true on brand new components:

public void installBindings(InteractionController controller) {
    Adapter[] adapters = controller.getAllAdaptersForTarget(this);

    if (adapters.length == 0) {

Then, you will need to construct the event script wrapping object, an ActionAdapter and set it up with all the required elements.
Again, taking from the Paintable Canvas:

ActionAdapter adapter = new ActionAdapter();
adapter.setTarget(this);
adapter.setJythonCode(writer.toString());
adapter.setBuilderMode(ActionAdapter.MODE_SCRIPT);
adapter.setBuilderInfo(null);
adapter.setEventSet(new EventSetDescriptor(PMIPaintableCanvas.class, "paint", PaintListener.class, "repaint"));
adapter.setMethodDescriptor(new MethodDescriptor(PaintListener.class.getMethod("repaint", PaintEvent.class)));
controller.addAdapter(this, adapter);

EventSetDescriptor and MethodDescriptor are standard Java "beans" introspection classes, not Ignition/Vision specific.

As far as I can tell, that's basically it. Whenever your component is constructed by Vision, if it happens to be a SelfBinder we'll automatically run that initialization code to allow you to configure it.

5 Likes

May i ask

[quote="PGriffith, post:7, topic:93735"]

ActionAdapter adapter = new ActionAdapter();
adapter.setTarget(this);
adapter.setJythonCode(writer.toString());
adapter.setBuilderMode(ActionAdapter.MODE_SCRIPT);
adapter.setBuilderInfo(null);
adapter.setEventSet(new EventSetDescriptor(PMIPaintableCanvas.class, "paint", PaintListener.class, "repaint"));
adapter.setMethodDescriptor(new MethodDescriptor(PaintListener.class.getMethod("repaint", PaintEvent.class)));
controller.addAdapter(this, adapter);

[ /quote]

May I ask, is the jython code kept in the writer object and if so how is this initialized? Is there any code available with the constructor used for this application?

Currently, in my code I have as instructed implemented SelfBinder in my component:

public class ButtonComponentTest extends AbstractVisionComponent implements SelfBinder

and before my setter and getter methods, I have implemented the installBindings method:

    //Section 5 - Adding the default jython script to the component.
    public void installBindings(InteractionController controller) {
    	Adapter[] adapters = controller.getAllAdaptersForTarget(this);
    	
    	String jythonString = "DisplayHandle = system.gui.getCurrentDesktop()\r\n" + 
        		"DesktopNav = system.nav.desktop(DisplayHandle)\r\n" + 
        		"parameters = {\"FolderRoot\": event.source.folderRoot}\r\n" + 
        		"DesktopNav.openWindow(event.source.windowPath, parameters)\r\n" + 
        		"DesktopNav.centerWindow(window)";
    	
    	if (adapters.length == 0) {
    		ActionAdapter adapter = new ActionAdapter();
    		adapter.setTarget(this);
    		adapter.setJythonCode(jythonString);
    		adapter.setBuilderMode(ActionAdapter.MODE_SCRIPT);
    		adapter.setBuilderInfo(null);
			try {
				adapter.setEventSet(new EventSetDescriptor(ButtonComponentTest.class, "action", ActionListener.class, "actionPerformed"));
			} catch (IntrospectionException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			try {
				adapter.setMethodDescriptor(new MethodDescriptor(ActionEvent.class.getMethod("actionPerformed", ActionListener.class)));
			} catch (NoSuchMethodException | SecurityException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}

    		controller.addAdapter(this, adapter);
    	}
    }

The module builds and installs, and I can place the custom component also the vision project browser indicates that I do in-fact have some form of script on the component. But, whenever I try to CTRL+J or right-click and select "Scripting...", absolutely nothing happens no IDE, nothing... See screenshot below of the component in the project browser:

image

See post below!

So after finally a colleague reminded me that the Console exists in Designer, I have determined that when placing the component I am getting a NoSuchMethod Exception and if I try to get onto the Scripting menu I get a java.lang.NullPointerException.

Current code is:

    //Section 5 - Adding the default jython script to the component.
    public void installBindings(InteractionController controller) {
    	Adapter[] adapters = controller.getAllAdaptersForTarget(this);

    	String jythonString = "DisplayHandle = system.gui.getCurrentDesktop()\r\n" + 
        		"DesktopNav = system.nav.desktop(DisplayHandle)\r\n" + 
        		"parameters = {\"FolderRoot\": event.source.folderRoot}\r\n" + 
        		"DesktopNav.openWindow(event.source.windowPath, parameters)\r\n" + 
        		"DesktopNav.centerWindow(window)";
    	
    	if (adapters.length == 0) {
    		ActionAdapter adapter = new ActionAdapter();
    		adapter.setTarget(this);
    		adapter.setJythonCode(jythonString);
    		adapter.setBuilderMode(ActionAdapter.MODE_SCRIPT);
    		adapter.setBuilderInfo(null);
			try {
				adapter.setEventSet(new EventSetDescriptor(MeterButtonComponent2.class, "action", ActionListener.class, "actionPerformed"));
			} catch (IntrospectionException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			try {
				adapter.setMethodDescriptor(new MethodDescriptor(MeterButtonComponent2.class.getMethod("actionPerformed", ActionEvent.class)));
			} catch (NoSuchMethodException | SecurityException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}

    		controller.addAdapter(this, adapter);
    	}
    }

and the error exists on the line adapter.setMethodDescriptor(new MethodDescriptor(MeterButtonComponent2.class.getMethod("actionPerformed", ActionEvent.class)));, in the second try-statement.

Anyone able to point me in the direction to getting this resolved?

Resolved: in the line mentioned above I should have been using adapter.setMethodDescriptor(new MethodDescriptor(ActionListener.class.getMethod("actionPerformed", ActionEvent.class)));

Thanks in advanced,

MG3.

1 Like