documentationfor yFiles for HTML 2.6

Low-level Customizations

Internally, reading and writing the data in the <data> elements is handled by so-called input and output handlers. Implementing the interfaces IInputHandler and IOutputHandler will provide the maximum flexibility for handling additional data.

Very roughly spoken, the IInputHandler.parseData method will be invoked for each <data> element which is associated with a <key> the handler is supposed to handle. The usual procedure is to get the current element, i.e. the graph element which is encoded by <data>’s parent element, parse the passed XML node (<data>’s child node), and associate the object created according to the node with the graph item. However, it is totally up to the developer whether and how to do this.

The other way around: The IOutputHandler.writeValue method will be invoked for each graph item the handler is supposed to handle. It should write the content for a <data> element which is associated with a <key> the handler is supposed to handle. The current graph item can be retrieved from the IWriteContext as well as the IXmlWriter which should be used to write XML as child node of the <data> element. Again, it is totally up to the developer whether and how to do this.

Implementing Custom I/O Handlers

Implementing a custom output handler is widely facilitated by extending the abstract base class OutputHandlerBase<TKey,TData>:

  • In the constructor: invoke the super constructor to provide the name and scope for the <key> definition
  • implement abstract method getValue to retrieve the object which should be written for the given graph item
  • implement abstract method writeValueCore to write the given data.

A custom output handler
class MyOutputHandler extends OutputHandlerBase {
  constructor() {
    super(IEdge.$class, MyTextHolder.$class, KeyScope.EDGE, 'my-custom-edge-data', KeyType.COMPLEX)
  }

  /**
   * @param {!IWriteContext} context
   * @param {!MyTextHolder} data
   */
  writeValueCore(context, data) {
    // serialize the data as XML
    const writer = context.writer
    writer.writeStartElement('MyTextHolder', 'MyNamespace')
    writer.writeCData(data.text)
    writer.writeEndElement()
  }

  /**
   * @param {!IWriteContext} context
   * @param {!IEdge} key
   * @returns {?MyTextHolder}
   */
  getValue(context, key) {
    // get the data which is associated with the current graph item (the key)
    return MyDataStore.getTextHolderForEdge(key)
  }
}class MyOutputHandler extends OutputHandlerBase<IEdge, MyTextHolder> {
  constructor() {
    super(IEdge.$class, MyTextHolder.$class, KeyScope.EDGE, 'my-custom-edge-data', KeyType.COMPLEX)
  }

  writeValueCore(context: IWriteContext, data: MyTextHolder): void {
    // serialize the data as XML
    const writer = context.writer
    writer.writeStartElement('MyTextHolder', 'MyNamespace')
    writer.writeCData(data.text)
    writer.writeEndElement()
  }

  getValue(context: IWriteContext, key: IEdge): MyTextHolder | null {
    // get the data which is associated with the current graph item (the key)
    return MyDataStore.getTextHolderForEdge(key)
  }
}

Data which can be serialized by the GraphMLIOHandler can be written using IWriteContext’s serialize<T> method.

Implementing a custom input handler is widely facilitated by extending the abstract base class InputHandlerBase<TKey,TData>:

  • implement method parseDataCore to return the object deserialized from the given XML node
  • implement method setValue to associate the given data with the given graph item (key)

A custom input handler
class MyInputHandler extends InputHandlerBase {
  constructor() {
    super(IEdge.$class, MyTextHolder.$class, ParsePrecedence.DEFAULT)
  }

  /**
   * @param {!IParseContext} context
   * @param {!Node} node
   * @returns {?MyTextHolder}
   */
  parseDataCore(context, node) {
    // gets the child of the <data> element to parse
    if (node instanceof Element) {
      const element = node
      if (element.localName === 'MyTextHolder' && element.namespaceURI === 'MyNamespace') {
        const text = element.textContent
        return new MyTextHolder(text)
      }
    }
    throw new Error('Invalid XML')
  }

  /**
   * @param {!IParseContext} context
   * @param {!IEdge} key
   * @param {!MyTextHolder} data
   */
  setValue(context, key, data) {
    // after the data has been deserialized associate it with the current graph item (key)
    MyDataStore.storeTextHolderForEdge(key, data)
  }
}class MyInputHandler extends InputHandlerBase<IEdge, MyTextHolder> {
  constructor() {
    super(IEdge.$class, MyTextHolder.$class, ParsePrecedence.DEFAULT)
  }

  parseDataCore(context: IParseContext, node: Node): MyTextHolder | null {
    // gets the child of the <data> element to parse
    if (node instanceof Element) {
      const element = node
      if (element.localName === 'MyTextHolder' && element.namespaceURI === 'MyNamespace') {
        const text = element.textContent
        return new MyTextHolder(text)
      }
    }
    throw new Error('Invalid XML')
  }

  setValue(context: IParseContext, key: IEdge, data: MyTextHolder) {
    // after the data has been deserialized associate it with the current graph item (key)
    MyDataStore.storeTextHolderForEdge(key, data)
  }
}

Data which can be deserialized by the GraphMLIOHandler can be parsed using IWriteContext’s deserialize<T> method.

Registering Custom I/O Handlers

Input and output handlers are registered at a GraphMLIOHandler during initialization by means of event handlers. Event handlers for both output handlers and input handlers may be registered using methods QueryOutputHandlers and QueryInputHandlers, respectively. The events are invoked by GraphMLIOHandler dynamically, i.e., at the time of writing or reading.

Attaching output handlers to a GraphMLIOHandler instance
graphMLIOHandler.addQueryOutputHandlersListener((sender, args) => {
  if (args.scope === KeyScope.EDGE) {
    // register the handler only for the matching scope
    args.addOutputHandler(myGetCustomEdgeOutputHandler())
  }
})

The event is dispatched for each scope before writing. Implementations should check the scope before adding the output handler. Note that the output handler itself is responsible for providing the correct attributes for the key element. OutputHandlerBase<TKey,TData>’s constructors therefore require at least the name as parameter.

Attaching input handlers to a GraphMLIOHandler instance
graphMLIOHandler.addQueryInputHandlersListener((sender, args) => {
  if (
    args.handled ||
    !GraphMLIOHandler.matchesName(args.keyDefinition, 'my-custom-edge-data') ||
    !GraphMLIOHandler.matchesScope(args.keyDefinition, KeyScope.EDGE)
  ) {
    // do nothing if
    //   args are already handled or
    //   'attr.name' is not "my-custom-edge-data"
    //   or scope is not edge
    return
  }
  args.addInputHandler(myGetCustomEdgeInputHandler())
  args.handled = true
})

The event is dispatched for each <key> element during parsing. Implementations should check whether the event is already handled by another input handler. Also, they have to check whether the <key> element’s name and scope attributes are matching before adding the input handler. After the handler has been added handled should be set to true.