documentationfor yFiles for HTML 2.6

Serializing Complex Types

If you want to serialize and deserialize your own types that can’t be serialized out of the box (as mentioned in Automatically Serialized Types), for example your business model or just your custom styles implementation, you need to explicitly provide means to serialize and deserialize them.

There are three different ways to provide serialization support for your classes.

Markup Extension

Markup extensions use the fact that the (de)serialization mechanism in yFiles for HTML uses reflection to discover readable and writable properties of classes and set their values.

A markup extension can be used to support (de)serialization for types which do not fulfill all prerequisites to be (de)serialized themselves. It does so by adapting the class it handles to expose all values to be serialized as read/write properties.

A markup extension is a class that derives from the abstract base class MarkupExtension. Creating one enables both the serialization and deserialization of its adapted class. When encountering an XML element of a class that is represented by a markup extension, the framework fills the properties of the markup extension with the values of the XML element. Afterwards it calls the abstract method provideValue and expects the markup extension to return an object of the desired class, given the values that are set in the properties.

Registering a Markup Extension for Deserialization

If the markup extension is required for deserialization, it needs to be registered with addXamlNamespaceMapping. The markup extension’s XML tag name must be the represented type’s XML tag name plus "Extension".

Registering a Markup Extension for Serialization

If the markup extension is required for serialization as well, you need to tell the framework that your particular class is represented by your markup extension for serialization. For this, you need to provide a way to acquire a markup extension from your custom type. This is accomplished by implementing the IMarkupExtensionConverter interface.

  • In its canConvert method return true for objects that the markup extension can handle.
  • In the convert method create and return your markup extension properly configured for the given object.

Finally, you need to provide your IMarkupExtensionConverter for your object:

The A sample class definition with a read-only property and a markup extension that adapts this class example shows a sample class definition for objects that we want to write to and read from GraphML along with the graph structure. The class has only one property, which is read-only and can only be set by the constructor. Thus objects of the class cannot be handled directly by XML serialization/deserialization which needs writable properties. As a consequence, we have implemented a markup extension which will be used for the serialization process instead. The markup extension overcomes the class’s limitations by exposing the property as read/write property and by providing a default constructor.

A sample class definition with a read-only property and a markup extension that adapts this class
class MyClass {
  $myProperty

  // non-default constructor prevents deserialization
  /**
   * @param {*} myValue
   */
  constructor(myValue) {
    this.$myProperty = myValue
  }

  // read-only property which is not (de)serializable
  /**
   * @type {*}
   */
  get myProperty() {
    return this.$myProperty
  }
}class MyClass {
  private readonly $myProperty: any

  // non-default constructor prevents deserialization
  constructor(myValue: any) {
    this.$myProperty = myValue
  }

  // read-only property which is not (de)serializable
  get myProperty(): any {
    return this.$myProperty
  }
}
// MarkupExtension for MyClass that exposes MyProperty as serializable read/write property
class MyClassExtension extends MarkupExtension {
  $myProperty = ''

  /**
   * @type {!string}
   */
  get myProperty() {
    return this.$myProperty
  }

  /**
   * @type {!string}
   */
  set myProperty(value) {
    this.$myProperty = value
  }

  // creates an instance of MyClass after deserialization
  /**
   * @param {!ILookup} lookup
   */
  provideValue(lookup) {
    return new MyClass(this.myProperty)
  }
}// MarkupExtension for MyClass that exposes MyProperty as serializable read/write property
class MyClassExtension extends MarkupExtension {
  private $myProperty = ''

  get myProperty(): string {
    return this.$myProperty
  }

  set myProperty(value: string) {
    this.$myProperty = value
  }

  // creates an instance of MyClass after deserialization
  provideValue(lookup: ILookup) {
    return new MyClass(this.myProperty)
  }
}

How an implementation of IMarkupExtensionConverter for the sample class and markup extension might look like is shown in the example An implementation of IMarkupExtensionConverter for the sample class.

An implementation of IMarkupExtensionConverter for the sample class
// markup extension converter converts MyClass into its markup extension
class MyClassMarkupExtensionConverter extends BaseClass(IMarkupExtensionConverter) {
  /**
   * Returns true if the converter can handle the given value
   * @param {!IWriteContext} context
   * @param {*} value
   * @returns {boolean}
   */
  canConvert(context, value) {
    // for the sake of demonstration, we simply check if the value is of type MyClass
    return value instanceof MyClass
  }

  /**
   * Converts an object of type MyClass into its markup extension
   * @param {!IWriteContext} context
   * @param {*} value
   * @returns {!MyClassExtension}
   */
  convert(context, value) {
    // create a new instance and set the MyProperty
    const extension = new MyClassExtension()
    extension.myProperty = value.myProperty
    return extension
  }
}// markup extension converter converts MyClass into its markup extension
class MyClassMarkupExtensionConverter extends BaseClass(IMarkupExtensionConverter) {
  /**
   * Returns true if the converter can handle the given value
   */
  canConvert(context: IWriteContext, value: any): boolean {
    // for the sake of demonstration, we simply check if the value is of type MyClass
    return value instanceof MyClass
  }

  /**
   * Converts an object of type MyClass into its markup extension
   */
  convert(context: IWriteContext, value: any): MyClassExtension {
    // create a new instance and set the MyProperty
    const extension = new MyClassExtension()
    extension.myProperty = value.myProperty
    return extension
  }
}

As previously mentioned, if you have the source code of the class you want to serialize, there are different ways to let the framework use the markup extension. The easiest way is to annotate the sample class directly and passing the IMarkupExtensionConverter implementation in the annotation as shown in the example Inserting the IMarkupExtensionConverter by annotating the sample class.

Inserting the IMarkupExtensionConverter by annotating the sample class
class MyClassES6 {
  // provide a static $meta property that returns an object with meta properties for each field
  static get $meta() {
    return {
      // '$self' is for the class itself
      $self: GraphMLAttribute().init({
        markupExtensionConverter: MyClassMarkupExtensionConverter.$class
      })
    }
  }
}

Similar to this example, it is also possible to annotate single properties with attributes.

Another way is to implement ILookup on the sample class and return the IMarkupExtensionConverter implementation in the lookup method as shown in example Inserting the IMarkupExtensionConverter by implementing ILookup.

Inserting the IMarkupExtensionConverter by implementing ILookup
// the same class, now implementing the lookup mechanism
class MyClass extends yfiles.lang.Class(yfiles.graph.ILookup) {


  ...


  lookup(requestedType) {
    if (yfiles.graphml.IMarkupExtensionConverter.$class === requestedType) {
      // if the framework is looking for an IMarkupExtensionConverter, return our converter.
      return new MyClassMarkupExtensionConverter();
    }
    return null;
  }
}

You can as well let the class itself return the markup extension by implementing the IMarkupExtensionConverter interface directly.

Inserting the IMarkupExtensionConverter by implementing IMarkupExtensionConverter directly
// The class itself provides the markup extension
class MyClass extends yfiles.lang.Class(yfiles.graphml.IMarkupExtensionConverter) {


  ...


  canConvert(context, value) { ... }


  convert(context, value) { ... }
}

In cases where you can’t alter the source code of the class to serialize, you have to insert the MarkupExtension in the serialization process yourself. This can be done by registering an event handler to the GraphMLIOHandler.HandleSerialization event. The crucial part here is to call the IWriteContext.serializeReplacement<T> method within the listener and replace the object to serialize with a markup extension for the object. The example Manually inserting the MarkupExtension in the serialization process shows how this can be implemented.

Manually inserting the MarkupExtension in the serialization process
graphMLIOHandler.addHandleSerializationListener((source, args) => {
  const item = args.item
  if (item instanceof MyClass) {
    const myClassObject = item
    const myClassMarkupExtension = new MyClassExtension()
    myClassMarkupExtension.myProperty = myClassObject.myProperty
    try {
      const context = args.context
      context.serializeReplacement(MyClassExtension.$class, myClassObject, myClassMarkupExtension)
    } catch (err) {
      console.error(err.message)
    }
    args.handled = true
  }
})
graphMLIOHandler.addHandleSerializationListener((source, args) => {
  const item = args.item
  if (item instanceof MyClass) {
    const myClassObject = item
    const myClassMarkupExtension = new MyClassExtension()
    myClassMarkupExtension.myProperty = myClassObject.myProperty
    try {
      const context = args.context
      context.serializeReplacement(MyClassExtension.$class, myClassObject, myClassMarkupExtension)
    } catch (err) {
      console.error((err as Error).message)
    }
    args.handled = true
  }
})

The GraphMLIOHandler.HandleSerialization event enables customization of the entire serialization process. This event, along with its counterpart for deserialization and the possibilities for customizations using these events are described in detail in the section Serialization Event Listeners.

Serialization Event Listeners

The GraphMLIOHandler.HandleSerialization and GraphMLIOHandler.HandleDeserialization events provide low-level hooks for customizing the (de)serialization process. They are dispatched each time the GraphML writing mechanism encounters an object to serialize or an XML element to deserialize.

They can be used in cases where objects need to be serialized that cannot be altered (as mentioned in the section Markup Extension) or where developers want to have full control over the resulting output.

A serialization handler is an event handler registered on the aforementioned events. If the object to be serialized can be handled, the provided XML writer has to be used to write XML representing the object. Afterwards, the handled flag has to be set to true so no other handler will be queried.

A simple serialization handler
function serializationHandler(source, args) {
  // only serialize items that are of the specific type
  if (args.item instanceof MyTextHolder) {
    const myItem = args.item
    const writer = args.writer
    writer.writeStartElement('MyTextHolder', 'MyNamespace')
    writer.writeCData(myItem.text)
    writer.writeEndElement()
    // Signal that this item is serialized.
    args.handled = true
  }
}

// register the custom serialization handler
graphMLIOHandler.addHandleSerializationListener(serializationHandler)
function serializationHandler(source: GraphMLIOHandler, args: HandleSerializationEventArgs): void {
  // only serialize items that are of the specific type
  if (args.item instanceof MyTextHolder) {
    const myItem = args.item
    const writer = args.writer
    writer.writeStartElement('MyTextHolder', 'MyNamespace')
    writer.writeCData(myItem.text)
    writer.writeEndElement()
    // Signal that this item is serialized.
    args.handled = true
  }
}

// register the custom serialization handler
graphMLIOHandler.addHandleSerializationListener(serializationHandler)

You need to register your custom serialization handlers using HandleSerialization.

A deserialization handler is an event handler for events which are dispatched each time the GraphML parsing mechanism retrieves an XML element that is about to be deserialized. If the handler recognizes the XML element as XML it can handle (usually if both the element’s name and namespace are matching), it has to create an instance of the class it handles based on the XML element. The created object has to be set as result which automatically sets the event as handled, preventing any subsequent handler from being invoked.

A simple deserialization handler
const deserializationHandler = (source, args) => {
  if (args.xmlNode instanceof Element) {
    const element = args.xmlNode
    if (element.localName === 'MyTextHolder' && element.namespaceURI === 'MyNamespace') {
      const text = element.textContent
      // setting the result sets the event arguments to handled
      args.result = new MyTextHolder(text)
    }
  }
}

// register the custom deserialization handler
graphMLIOHandler.addHandleDeserializationListener(deserializationHandler)
const deserializationHandler = (source: GraphMLIOHandler, args: HandleDeserializationEventArgs): void => {
  if (args.xmlNode instanceof Element) {
    const element = args.xmlNode
    if (element.localName === 'MyTextHolder' && element.namespaceURI === 'MyNamespace') {
      const text = element.textContent
      // setting the result sets the event arguments to handled
      args.result = new MyTextHolder(text)
    }
  }
}

// register the custom deserialization handler
graphMLIOHandler.addHandleDeserializationListener(deserializationHandler)

You need to register your custom deserialization handlers using HandleDeserialization.

Serialization with Symbolic Names

It is possible to use symbolic names for the output file format instead of XML elements that encode the information of an object. Using this approach, you shift the responsibility to encode the information from the file format to the application. These so-called external references are specified by symbolic names. To this end, event handlers that provide the symbolic names may be registered using method QueryReferenceId as shown below:

Writing an external reference using a symbolic name
graphMLIOHandler.addQueryReferenceIdListener((sender, /* QueryReferenceIdEventArgs */ args) => {
  // If the current object is the same as the default node style, then write
  // "DefaultNodeStyle" instead.
  if (args.value === args.context.graph.nodeDefaults.style) {
    args.referenceId = 'DefaultNodeStyle'
  }
})

The symbolic name is specified using the referenceId property provided by the event argument’s type that is handed over to the event handler. It is then used in GraphML in the <data> element as shown below:

External reference in the <data> element
<node id="n1">
  <data key="d2">
<!-- An external reference. The value of the 'ResourceKey' attribute is a -->
<!-- user-defined symbolic name. -->
    <y:GraphMLReference ResourceKey="DefaultNodeStyle" />
  </data>
  ...
</node>
   

When reading in the GraphML representation of a graph, an application can be notified of external references by registering event handlers using method ResolveReference. Corresponding events are triggered for references that cannot be resolved, which is especially true for external references.

Resolving an external reference using a symbolic name
graphMLIOHandler.addResolveReferenceListener((sender, args) => {
  if (args.referenceId === 'DefaultNodeStyle') {
    // If we encounter a "DefaultNodeStyle" reference, retrieve an instance
    // from the graph's NodeDefaults.
    args.value = args.context.graph.nodeDefaults.getStyleInstance()
  }
})