documentationfor yFiles for HTML 3.0.0.1

Low-level Customizations

Internally, reading and writing the data in the <data> elements is handled by input and output handlers. Implementing the interfaces IInputHandler and IOutputHandler provides the most flexibility for handling additional data.

Roughly speaking, the IInputHandler.parseData method is invoked for each <data> element that is associated with a <key> the handler is designed to handle. The typical procedure is to get the current element (the graph element encoded by the <data> element’s parent element), parse the passed XML node (the <data> element’s child node), and associate the created object with the graph item. The way this is done is up to the developer.

On the other hand, the IOutputHandler.writeValue method is invoked for each graph item the handler is designed to handle. It should write the content for a <data> element that 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 a child node of the <data> element. Again, the implementation is up to the developer.

Implementing Custom I/O Handlers

Implementing a custom output handler is greatly simplified 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 the abstract method getValue to retrieve the object that should be written for the given graph item.
  • Implement the abstract method writeValueCore to write the given data.

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

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

  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,
      MyTextHolder,
      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 that can be serialized by the GraphMLIOHandler can be written using IWriteContext’s serialize<T> method.

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

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

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

  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')
  }

  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, MyTextHolder, 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 that 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 with a GraphMLIOHandler during initialization using event handlers. You can register event handlers for both output and input handlers using the query-output-handlers and query-input-handlers methods, respectively. The events are invoked by GraphMLIOHandler dynamically, that is, at the time of writing or reading.

Attaching output handlers to a GraphMLIOHandler instance
graphMLIOHandler.addEventListener('query-output-handlers', (evt) => {
  if (evt.scope === KeyScope.EDGE) {
    // register the handler only for the matching scope
    evt.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 is responsible for providing the correct attributes for the key element. OutputHandlerBase<TKey,TData>’s constructors, therefore, require at least the name as a parameter.

Attaching input handlers to a GraphMLIOHandler instance
graphMLIOHandler.addEventListener('query-input-handlers', (evt) => {
  if (
    evt.handled ||
    !GraphMLIOHandler.matchesName(evt.keyDefinition, 'my-custom-edge-data') ||
    !GraphMLIOHandler.matchesScope(evt.keyDefinition, KeyScope.EDGE)
  ) {
    // do nothing if
    //   evt are already handled or
    //   'attr.name' is not "my-custom-edge-data"
    //   or scope is not edge
    return
  }
  evt.addInputHandler(myGetCustomEdgeInputHandler())
  evt.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 match before adding the input handler. After the handler has been added, handled should be set to true.