Ignition SDK Programmer's Guide
How-to Articles
Strategic Partner Links
Sepasoft - MES Modules
Cirrus Link - MQTT Modules
Resources
Inductive University
Ignition Demo Project
Knowledge Base Articles
Forum
IA Support
SDK Examples
The following is a listing of notable API changes introduced to Ignition version 8.1.0+
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. 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 } }
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.
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.
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(); } }
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.
notify
on the delegate to get it to update.
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 } }
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.
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.