Custom Vision Component

I am trying to create a Vision Component that has a Numeric Label inside it. The component itself has two custom properties: tagPath and horizontalAlign. I am having trouble binding the tag value at tagPath to the value of the numeric label. This is the Java file for the component:

package com.inductiveautomation.ignition.examples.ce.components;

import com.inductiveautomation.factorypmi.application.binding.SimpleBoundTagAdapter;
import com.inductiveautomation.factorypmi.application.components.PMINumericLabel;
import com.inductiveautomation.vision.api.client.components.model.AbstractVisionComponent;

import java.awt.*;


public class AnalogValueComponent extends AbstractVisionComponent {

    private PMINumericLabel valueLabel;
    private String tagPath;
    private int horizontalAlign;
    private SimpleBoundTagAdapter bindingAdapter;

    public AnalogValueComponent() {
        setLayout(new BorderLayout());
        valueLabel = new PMINumericLabel();
        add(valueLabel, BorderLayout.CENTER);
    }

    public static void main(String[] args) {
        new AnalogValueComponent();
    }

    public String getTagPath() {
        return this.tagPath;
    }

    public void setTagPath(String newTagPath) {
        String old = this.tagPath;
        this.tagPath = newTagPath;
        firePropertyChange("tagPath", old, newTagPath);
    }

    public int getHorizontalAlign() {
        return this.horizontalAlign;
    }

    public void setHorizontalAlign(int newHorizontalAlign) {
        int old = this.horizontalAlign;
        this.horizontalAlign = newHorizontalAlign;
        firePropertyChange("horizontalAlign", old, newHorizontalAlign);
    }

    @Override
    protected void onStartup() {
        super.onStartup();
        bindingAdapter = new SimpleBoundTagAdapter();
        bindingAdapter.setTarget(valueLabel);
        bindingAdapter.setTargetPropertyName("value");
        bindingAdapter.setTagPathString(this.tagPath);
        bindingAdapter.startup();
    }

    @Override
    protected void onShutdown() {
        super.onShutdown();
        bindingAdapter.shutdown();
    }
}

BeanInfo file


package com.inductiveautomation.ignition.examples.ce.beaninfos;

import com.inductiveautomation.factorypmi.designer.property.customizers.DynamicPropertyProviderCustomizer;
import com.inductiveautomation.factorypmi.designer.property.customizers.StyleCustomizer;
import com.inductiveautomation.ignition.examples.ce.components.AnalogValueComponent;
import com.inductiveautomation.vision.api.designer.beans.CommonBeanInfo;
import com.inductiveautomation.vision.api.designer.beans.VisionBeanDescriptor;

import java.beans.IntrospectionException;

public class AnalogValueComponentBeanInfo extends CommonBeanInfo {

    public AnalogValueComponentBeanInfo() {
        super(AnalogValueComponent.class, DynamicPropertyProviderCustomizer.VALUE_DESCRIPTOR, StyleCustomizer.VALUE_DESCRIPTOR);
    }

    @Override
    public void initProperties() throws IntrospectionException {
        super.initProperties();
        removeProp("opaque");
        removeProp("font");
        removeProp("foreground");
        removeProp("background");

        addProp("tagPath", "tagPath", "Tag Path for template component bindings", "Custom",
                PREFERRED_MASK | BOUND_MASK);
        addProp("horizontalAlign", "horizontalAlign", "horizontalAlign for template component " +
                "bindings", "Custom", PREFERRED_MASK | BOUND_MASK);

    }

    @Override
    protected void initDesc() {
        VisionBeanDescriptor bean = getBeanDescriptor();
        bean.setDisplayName("Analog Value Component");
        bean.setShortDescription("Analog Value component");
        bean.setValue(CommonBeanInfo.TERM_FINDER_CLASS, AnalogValueComponentTermFinder.class);
    }

}

The value doesn't appear in the component.

I also get an error in the console whenever I create a new instance of the component:

ERROR com.inductiveautomation.ignition.examples.ce.components.AnalogValueComponent - Error starting up "AnalogValueComponent"
java.lang.NullPointerException: null
	at com.inductiveautomation.factorypmi.application.binding.SimpleBoundTagAdapter.startup(SimpleBoundTagAdapter.java:154)
	at com.inductiveautomation.ignition.examples.ce.components.AnalogValueComponent.onStartup(AnalogValueComponent.java:55)
	at com.inductiveautomation.vision.api.client.components.model.AbstractVisionComponent.startupComponent(AbstractVisionComponent.java:191)
	at com.inductiveautomation.factorypmi.application.components.util.ComponentVisitor$StartupVisitor.visit(ComponentVisitor.java:344)
	at com.inductiveautomation.factorypmi.application.components.util.ComponentVisitor.walk(ComponentVisitor.java:95)
	at com.inductiveautomation.factorypmi.application.components.util.ComponentVisitor.walk(ComponentVisitor.java:73)
	at com.inductiveautomation.factorypmi.application.FPMIWindow.startup(FPMIWindow.java:347)
	at com.inductiveautomation.factorypmi.designer.workspace.WindowWorkspace$DesigntimeWindowOpener.openWindow(WindowWorkspace.java:3880)
	at com.inductiveautomation.factorypmi.designer.workspace.WindowWorkspace.openWindow(WindowWorkspace.java:1528)
	at com.inductiveautomation.factorypmi.designer.model.navtree.WindowNode.open(WindowNode.java:260)
	at com.inductiveautomation.ignition.designer.navtree.model.AbstractResourceNavTreeNode.onDoubleClick(AbstractResourceNavTreeNode.java:428)
	at com.inductiveautomation.ignition.designer.navtree.NavTreePanel$MouseListener.lambda$mousePressed$0(NavTreePanel.java:716)
	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)

Any help on how to fix this would be appreciated!

Don't do that. Bindings should be created by your users, not your component.

If you want to read a tag based on an input tagpath, you're eschewing the binding system and should go directly to the tag system:
com.inductiveautomation.ignition.common.tags.model.TagManager#subscribeAsync(com.inductiveautomation.ignition.common.tags.model.TagPath, com.inductiveautomation.ignition.common.tags.model.event.TagChangeListener)
Where you likely want to make your component itself a TagChangeListener.

3 Likes

But why is binding in a module a problem for Ignition? Does it cause unexpected behavior or instability?

The simplest answer is that we never designed the binding system in Vision to be directly usable by third parties; we want it to remain exclusively the domain of the end user(s) working out of the designer. Modeling your components as simple collections of properties and allowing end users flexibility is, in our official opinion, strictly better than the alternative.

Leaving Perspective out of the equation, where things are fundamentally a bit different, even in Vision you constantly have users asking for customization of the more "all in one" components like the easy chart, alarm status table, user management, etc. Inevitably, no matter how well constrained you think your problem is, someone will come up with some edge case. If your module delivers a component that contains all the business logic internally, then that one-off case requires an entire new module build, install, replacement loop. If it's a "dumb" bag of properties, it's easy to override behavior in that one-off case.

There are still opportunities to encapsulate business logic within expected bounds, such as adding system functions to scripting, custom expression functions, etc - which will naturally tie into the rest of Ignition much better. You can also add new binding types to Vision, to some extent, though I'll be honest and say I don't know if anyone ever actually exercised that capability.

I investigated that at one point, but found that extension point no better than expression functions, and limited to Vision. :man_shrugging:

I would like a platform-wide extension point for async binding types (like tag and query bindings).

Yeah I tried this once too, wouldn't recommended it.
Was looking to bind against a custom resource type, and ended up just using expression functions since it was easier and more flexible from every angle.

Even though it was not designed to be used that way, can it still be used that way? The only reason we are doing this is to deliver a completely functional (no need to be modified) controller of a highly complex liquid processing machine. We want to use all the wonderful abilities of Ignition to represent and build this controller, but not require the user to understand the inner workings of the machine or Ignition project. This project is, in the purest form, a turnkey controller that should not be configured by the user.

That being said, we don't want to create any custom properties or binding types. We only want to use the bindings natively found in Ignition, but within a module.

:man_shrugging:

It's just code, anything is possible. As Phil will attest, Vision's binding system is both complicated and fragile, and you have close to zero insight on how it works as a third party. Even with full access to the source code, it's hard to understand.

I would recommend a bit of an end-run - instead of trying to encapsulate everything inside a component, have your module do the also unsupported but much more feasible thing and drop an inheritable project on the filesystem when you install it. Design your windows/templates/whatever using regular Ignition tools, then mark them as 'Protected' (to minimize the ability of downstream users to change them), then export the project. Then wrap that export into your module in some way, splat it onto the filesystem, and train your end users to use that 'parent' project as the basis for their individual projects.

Then deliver the Ignition Server fully populated with the desired project. :man_shrugging:

1 Like

I understand and appreciate that the binding system is fragile and complicated and concede to your way of thinking. With the solution you provided to OP, is there also ways to program in this way with expressions, queries, etc? Can you point me to documentation for creating these types of pseudo-bindings inside a module? If the documentation is unavailable, could you provide short snippets of code for each pseudo-binding type (perhaps in a DM)? We have utilized almost every native binding type in Ignition to create this controller. Although simple and elegant, dropping the entire 'protected' project file onto the filesystem simply is not secure enough for our situation.