Skip to end of metadata
Go to start of metadata

The following is a listing of notable API changes introduced to Ignition version 8.1.0+

Perspective

Component Authoring

Removal of MobX from the PropertyTree

The PropertyTree no longer uses MobX. The intentions of this effort is to make Perspective more lightweight, performant, and less reliant on third-party libraries. By following a simpler subscription pattern, we've managed to reduce a considerable amount of overhead, resulting in a net increase in performance.  Furthermore, removing dependencies from PropertyTree well open up further opportunities in performance improvement exploration.

The removal of MobX means that components that are dereferencing nodes of a PropertyTree in MobX defined reactive functions (i.e. render, computed, reaction, autorun, etc.) can no longer expect that these reactive functions will be triggered when the dereferenced node's value changes. The PropertyTree now makes use of a simpler subscribe and notify  pattern. To subscribe to PropertyTree changes directly, use the subscribe method on the instance:

  • subscribe(listener: PropertyTreeChangeListener<unknown>): PropertyTreeChangeListenerDisposer - A public method used to subscribe a listener to any updates to the tree.  The listener takes a tree as an argument.  Implementers can use this to re-read from the tree and do whatever they would like with the new values.  Returns a disposer function that can be used by components to remove the listener from the tree's list of listeners in order to prevent memory leaks and no longer subscribe to changes.

In addition, the PropertyTree adds a new read method:

  • readObject(path: string, defaultValue = {}): PlainObject - A new PropertyTree read method. 

PropertyUtil

Introduced PropertyUtil, a named export of the perspective-client package. The namespace contains utility functions that are used by the PropertyTree to coerce Node values to a requested type. These are the inner workings of PropertyTree read methods. This was needed for components that would use the PropertyTree to read one or many nested properties of objects. Using utilities such as isNumber and isString were found to be too strict since they do not coerce the value.  PropertyUtil was created to prevent regressions and maintain existing behavior when evaluating nested properties of an object.

As mentioned, PropertyUtil contains several utility functions that coerce values. These functions are the literal logic used by the PropertyTree when performing reads. For example:

function getBooleanOrDefault(value, defaultValue) {
    if (typeof value === 'boolean') {
        // return if its already a boolean
    } else if (typeof value === 'number') {
        // return if it's not 0
    } else if (typeof value === 'string') {
        // return if the string is 'true'
    } else {
        // return the default
    }
}

Perspective Components  

In addition to React class components, top level Perspective components can now also be functional components.  This is exciting news, as component authors can now make use of the React Hooks API

ComponentProps

The shape of ComponentProps has changed. A component's props no longer contain instances of PropertyTree at the root level.  To access trees, use must go through the corresponding ComponentStore or store prop. 

For example, the new approach is:

this.props.store.props.write('value', newValue) 

For comparison, this was the old approach: 

this.props.props.write(‘value’, newValue)`)

This means that this.props.props  (or props.props  for functional components) now points to a prop shape declared in the components ComponentMeta  (see below).  

Example of a class component declaration:

class MyComponent extends Component<ComponentProps<MyComponentProps, MyOptionalComponentStoreDelegateState>, MyComponentState> 


Example of a functional component declaration (using arrow syntax):  

const MyComponent = (props: ComponentProps<MyComponentProps, MyOptionalComponentStoreDelegateStage>) => 


Where, in the example of the class component declaration, MyComponentProps  gets mapped to this.props.props, and MyOptionalComponentStoreDelegate gets mapped to this.props.delegate. The shape of the delegate prop is declared by the components corresponding ComponentStoreDelegate if any.

ComponentMeta  

Two changes were made to ComponentMeta. First, the function getViewClass  was renamed to getViewComponent.

getViewComponent(): PComponent -  Renamed from getViewClass  to be more inclusive of functional components. Expected return type is PComponent  and no longer React.ReactType .

Second, the addition of a reducer function called getPropsReducer

getPropsReducer(tree: PropertyTree): MyComponentProps - A reducer function that reads from the components PropertyTree and returns the shape of the props property of a component's React props (i.e. this.props.props  instance of MyComponentProps). For example, a component's meta should now look something like this.

export class MyComponentMeta implements ComponentMeta {
    // Renamed from getViewClass. Returns a PComponent 
    // which is either a class or functional Perspective component.
    getViewComponent(): PComponent {
        return Label;

    // Reducer function whose return value gets mapped to 
    // 'this.props.props' for class components or 'props.props'
    // for functional components
    getPropsReducer(tree: PropertyTree): MyComponentProps {
        return {
            value: tree.readString("value"),
        };
    }    

    getComponentType(): string {
        return ID;
    }    

    getDefaultSize(): SizeObject {
        return { width: 50, height: 50 };
    }
}


In this example, the shape of MyComponentProps can be an empty object. In this case, you can subscribe to the component's PropertyTree's manually in the component's constructor, or the equivalent for functional components. For example:

class MyComponent extends ... {
    propTreeDisposer: Function;

    componentDidMount() {
        // Subscribe to changes
        this.propTreeDisposer = this.props.store.props.subscribe(this.onTreeChange)
    }
    
    onTreeChange(tree: PropertyTree) {
        // React to changes
    }
   
    componentWillUnmount() {
        // Unsubscribe or dispose listener
        this.propTreeDisposer();
    }
}
ComponentStoreDelegate  

The ComponentStoreDelegate also takes on this new subscribe and notify pattern. Since it's a store, it uses the subscriptionFactory function described below.  It does so under the hood as a convenience. It does not allow for specific state change events via a subscription map, but instead any state change events.

  • mapStateToProps(): PlainObject | undefined - A public method of the abstract ComponentStoreDelegate class. Delegate authors can override this in order to map the delegate state to a component's delegate prop. The corresponding ComponentStore will use this method to map delegate state to props when notified of changes. This is optional.  If not used, the components delegate prop will be an empty object. 
  • subscribe(fn: Function): Function | undefined - A public method of the abstract ComponentStoreDelegate class. The corresponding ComponentStore will use this to subscribe to any delegate state changes.
  • notify(): void - A "protected" method of the abstract ComponentStoreDelegate class. Delegate authors must use this in their delegate implementations to notify corresponding components of changes, or any subscribers for that matter.

Note: You may continue to use MobX within stores, but the corresponding component shouldn't be expected to react to changes to dereferenced properties.  When a reactive function is invoked, you would call notify on the delegate to get it to update.  

Stores

As mentioned above, stores can still use MobX if they would like, but it cannot be expected that Perspective components will react to observables unless they are subscribed to notifications and properly notified when the observable value changes.  We provide a convenience function that can be used to set up a subscription pattern on a store called subscriptionFactory. It accepts a subscription map and returns the subscribe and notify methods for you.  The subscription map can include subscriptions to any events, specific events, or both. 

  • subscriptionFactory(map: SubscriptionMap, ownerName: string): SubscriptionFactory - Creates a SubscriptionFactory object containing notification and subscription handlers built using the provided subscription map object.  The subscription map holds references to the listeners and handlers, and as such, should not be modified once provided.  Modifying the subscription map will likely result in memory leaks and errors. Returns a SubscriptionFactory of the shape:  

   {
        // A public function of the owner used to subscribe to changes 
        // in the owners or subscription factory creator's state.
        subscribe: (fn: Function, state?: string, options?: { disposeWhenTrue: boolean }) => Function | undefined        

        // Invoked by the owner or subscription factory creator to notify any listeners or 
        // handlers for given state change events. If the map includes the special `anyChange` key.
        // Listeners that did not specify a state change event upon subscribing will also be notified.

        notify: (state?: string) => void
    }


  • createSubscriptionMap(stateEventNames: Array<string>, anyChange: boolean, customHandlers?: Record<string, SubscriptionHandler>): SubscriptionMap - A utility function to use in junction with subscriptionFactory.  Creates a subscription map that can be passed to the subscriptionFactory function. Provide an array of state event names which are used to create and map subscriptions and notifications.  

When you put this all together, an example might look something like this:  

// State events definition to share with subscribers
export enum SomeStoreState {
    someState = 'someState'
}

class SomeStore {
    // Initialize subscription map
    private subscriptionMap: SubscriptionMap = createSubscriptionMap(Object.values(SomeStoreState));  
    readonly subscribe: SubscriptionHandler;
    private notify: NotificationHandler;

    constructor() {
        const {
            subscribe,
            notify
        } = subscriptionFactory(this.subscriptionMap, 'SomeStore');
        this.subscribe = subscribe;
        this.notify = notify;
    }

    onSomeStateChange() {
        this.notify(SomeStoreState.someState);
    }
}

class ICareAboutSomeStoreChanges {
    disposer: Function;
    constructor(store: SomeStore) {
        this.disposer = store.subscribe(this.onSomeStoreStateChange, SomeStoreState.someState);    
    }

    onSomeStoreStateChange() {
        // Do stuff
    }
}

Platform

Tag Historian API

All Tag History related functionality is now protected by the Tag Historian Module. This includes functionality that may be extended by 3rd party modules, such as registering a new custom data sink, and querying data.

This means that all modules that provide tag historian functionality will require a valid license for the Tag Historian Module in order to continue working. For various reasons, most customers that use custom historian modules likely have such licenses already, but if not, they will need to contact Sales before upgrading in order to ensure that their license will allow this functionality to continue working.

The Tag Historian system provides significant value, even in abstract form, to users of Ignition. The built in support for querying in bindings, charting, tables, scripting and beyond- across local and remote gateways- is an important and significant feature of Ignition. While many aspects of these features are defined by the platform, the core implementation is achieved with the Tag Historian Module, and we believe that 3rd party offerings are an extension of that functionality. From a technical point of view, nothing has changed for module authors at this time- there is no need to define a concrete dependency on the Tag Historian Module- though module authors may want to do this, in order to avoid confusion on the part of the user.

OPC UA

Device API

Ignition 8.1 includes Milo version 0.5.0, which has changes to its AddressSpace and AddressSpaceComposite APIs. Drivers implemented against the new Device API may need to be recompiled to accommodate these changes.

Implementations built against the legacy Driver API do not require any changes.

  • No labels