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), such as your business model or your custom styles implementation, you need to explicitly provide a way to serialize and deserialize them.
There are three different ways to provide serialization support for your classes.
Markup Extension
Markup extensions leverage the (de)serialization mechanism in yFiles for HTML, which uses reflection to discover readable and writable properties of classes and set their values.
A markup extension supports (de)serialization for types that don’t meet all the prerequisites for being (de)serialized directly. It adapts 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 the framework encounters an XML element of a class that is represented by a markup extension, it fills the properties of the markup extension with the values from the XML element. Then, it calls the abstract method provideValue and expects the markup extension to return an object of the desired class, using the values that are set in the properties.
Since the markup extension replaces the actual object for (de)serialization purposes, it needs to be:
- Eligible for automatic (de)serialization.
- Properly mapped to an XML namespace. You do not have to specify an XML tag name. If you do so, you can strip the "Extension" part of the name.
Registering a Markup Extension for Deserialization
Any markup extension that has been properly mapped to an XML namespace will be used automatically for deserialization.
Registering a Markup Extension for Serialization
If the markup extension is also required for serialization, you need to tell the framework that your particular class is represented by your markup extension for serialization. To do this, you need to provide a way to acquire a markup extension from your custom type. This is done by providing additional type information with addTypeInformation<T>:
- Implement your markup extension.
- Specify a conversion function for the type information’s
extension
property. The function should return an instance of the custom markup extension if the provided object should be proxied by the custom extension, orundefined
if no extension should be created for this specific instance.
This 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. Therefore, objects of the class cannot be handled directly by XML serialization/deserialization, which needs writable properties. As a result, we have implemented a markup extension that will be used for the serialization process instead. The markup extension overcomes the class’s limitations by exposing the property as a read/write property and by providing a default constructor.
class MyClass {
$myProperty
// non-default constructor prevents deserialization
constructor(myValue) {
this.$myProperty = myValue
}
// read-only property which is not (de)serializable
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 = ''
get myProperty() {
return this.$myProperty
}
set myProperty(value) {
this.$myProperty = value
}
// creates an instance of MyClass after deserialization
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)
}
}
// markup extension converter converts MyClass into its markup extension
graphMLIOHandler.addTypeInformation(MyClass, {
extension: (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
graphMLIOHandler.addTypeInformation(MyClass, {
extension: (value: MyClass) => {
// create a new instance and set the MyProperty
const extension = new MyClassExtension()
extension.myProperty = value.myProperty
return extension
}
})
Serialization Event Listeners
The GraphMLIOHandler.handle-serialization and GraphMLIOHandler.handle-deserialization events provide low-level hooks for customizing the serialization and deserialization process. These events are triggered each time the GraphML writing mechanism encounters an object to serialize or an XML element to deserialize.
These events are useful when you need full control over the resulting output, for example, if the serialization format needs to be consumed or provided by external code.
A serialization handler is an event handler registered for the
aforementioned events. If the object to be serialized can be handled by a handler, the provided XML writer must be used
to write XML that represents the object. After writing the XML, set the handled flag to true
to prevent other
handlers from being queried.
function serializationHandler(evt) {
// only serialize items that are of the specific type
if (evt.item instanceof MyTextHolder) {
const myItem = evt.item
const writer = evt.writer
writer.writeStartElement('MyTextHolder', 'MyNamespace')
writer.writeCData(myItem.text)
writer.writeEndElement()
// Signal that this item is serialized.
evt.handled = true
}
}
// register the custom serialization handler
graphMLIOHandler.addEventListener(
'handle-serialization',
serializationHandler
)
function serializationHandler(evt: HandleSerializationEventArgs): void {
// only serialize items that are of the specific type
if (evt.item instanceof MyTextHolder) {
const myItem = evt.item
const writer = evt.writer
writer.writeStartElement('MyTextHolder', 'MyNamespace')
writer.writeCData(myItem.text)
writer.writeEndElement()
// Signal that this item is serialized.
evt.handled = true
}
}
// register the custom serialization handler
graphMLIOHandler.addEventListener(
'handle-serialization',
serializationHandler
)
Register your custom serialization handlers using handle-serialization.
A deserialization handler is an event handler for events that are triggered each time the GraphML parsing mechanism retrieves an XML element that is about to be deserialized. If a handler recognizes the XML element as XML it can handle (typically if the element’s name and namespace match), it must create an instance of the class it handles based on the XML element. The created object must be set as the result, which automatically sets the event as handled and prevents any subsequent handler from being invoked.
const deserializationHandler = (evt) => {
if (evt.xmlNode instanceof Element) {
const element = evt.xmlNode
if (
element.localName === 'MyTextHolder' &&
element.namespaceURI === 'MyNamespace'
) {
const text = element.textContent
// setting the result sets the event arguments to handled
evt.result = new MyTextHolder(text)
}
}
}
// register the custom deserialization handler
graphMLIOHandler.addEventListener(
'handle-deserialization',
deserializationHandler
)
const deserializationHandler = (
evt: HandleDeserializationEventArgs
): void => {
if (evt.xmlNode instanceof Element) {
const element = evt.xmlNode
if (
element.localName === 'MyTextHolder' &&
element.namespaceURI === 'MyNamespace'
) {
const text = element.textContent
// setting the result sets the event arguments to handled
evt.result = new MyTextHolder(text)
}
}
}
// register the custom deserialization handler
graphMLIOHandler.addEventListener(
'handle-deserialization',
deserializationHandler
)
Register your custom deserialization handlers using handle-deserialization.
Serialization with Symbolic Names
It is possible to use symbolic names in the output file format instead of XML elements that encode the information of an object. Using this approach, you shift the responsibility for encoding 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 query-reference-id as shown below:
graphMLIOHandler.addEventListener('query-reference-id', (evt) => {
// If the current object is the same as the default node style, then write
// "DefaultNodeStyle" instead.
if (evt.value === evt.context.graph.nodeDefaults.style) {
evt.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:
<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 resolve-reference. Corresponding events are triggered for references that cannot be resolved, which is especially true for external references.
graphMLIOHandler.addEventListener('resolve-reference', (evt) => {
if (evt.referenceId === 'DefaultNodeStyle') {
// If we encounter a "DefaultNodeStyle" reference, retrieve an instance
// from the graph's NodeDefaults.
evt.value = evt.context.graph.nodeDefaults.getStyleInstance()
}
})