Service Locator Pattern: Lookup
yFiles for HTML implements the service locator pattern in many of its core interfaces and elements. The main advantage of this pattern in the context of yFiles for HTML is that it shifts the responsibility for handling an element from the operating part to the element itself. The main benefits of using this pattern are:
- Flexibility: The logic for handling an element can be more easily changed. Instead of having one central piece of code that handles everything, the central piece asks the elements how they want to be handled.
- Scalability: Adding new functionality does not necessarily require changes to existing code.
- Simplification: Each part is smaller, simpler, and therefore easier to understand and maintain.
However, there are also disadvantages to this pattern. For one, it is not immediately obvious which objects an item returns during a lookup. Fortunately, it is rare that you actually need to use lookup to obtain objects. There are almost always other ways to get the information you need. The service locator pattern in yFiles for HTML is primarily intended as an anchor for injecting your own implementations while keeping the interfaces short and avoiding the need to derive classes. On the other hand, it is hard to guess what the framework expects an object to return in different situations just by looking at the available API. Using the service locator mechanism requires careful study of the available documentation, both in the API documentation and the Developer’s Guide.
This chapter covers an advanced topic. For most use cases, the mechanism described in the chapter Decorating Graph Elements can be used. It is recommended to read that chapter first if you are not already familiar with the topic.
One example where developers need to retrieve an object via lookup is when retrieving information from an IInputModeContext, as shown below in the example Example for a lookup.
One example where the documentation describes the use of the service locator pattern for a kind of interaction is section Customizing Resizing Nodes in this document, which lists the interfaces that are queried via lookup during resize operations.
Using Lookup
In yFiles for HTML, the primary interface that embodies the service locator pattern is the ILookup. Classes such as GraphComponent, IGraph, INode, and IEdge all implement this interface. This implementation enables them to provide various objects that the framework primarily uses to retrieve the information needed to handle the element in question.
The ILookup interface defines just one method:
lookup(type: Constructor): Object.
This method is called to request an instance of a specific type from the ILookup implementor.
The ILookup implementor then returns an instance of that type, or null
if no such instance is available.
A common scenario where a developer needs to query an ILookup implementor
is when querying an IInputModeContext's lookup
within a user interaction method for input mode related types.
Example for a lookup provides a common example:
The code calls the IInputModeContext to request an implementation of the
SnapContext class. The method returns an object of the
requested type, or null
if it is not available.
const snapContext = inputModeContext.lookup(SnapContext)
The Lookup Chain
All lookups of classes that implement the ILookup interface can be customized via decoration.
The primary lookup decoration mechanism relies on the usage of the IContextLookupChainLink interface, which is closely related to ILookup, but extends the concept in two ways:
- Adds lookup capabilities to any item
- Adds the ability to chain lookups, which means that if the first lookup does not satisfy the request, then the next lookup in the chain will be called, until the end of the chain is reached.
Decorating a lookup is then a simple matter of adding a chain link to the start of the lookup chain. You can do this using the ILookupDecorator interface, which you can obtain from all items that implement ILookup in yFiles for HTML via lookup.
The most important method defined on this interface is: addLookup(t: Constructor, lookup: IContextLookupChainLink)
This method takes an IContextLookupChainLink and adds it to the existing lookup chain of the given class. When the lookup mechanism of an object of this class is used to query something, the last added IContextLookupChainLink is called first.
Instead of creating an IContextLookupChainLink implementation and adding it to an ILookupDecorator directly, you should use the convenience methods of LookupDecorator<TDecoratedType,TInterface>.
For most common interfaces that can be decorated for graph elements, the more convenient LookupDecorator<TDecoratedType,TInterface> in combination with the GraphDecorator should be used. This is explained in detail in the section Decorating Graph Elements.
Even for other interfaces that shall be decorated for graph items, the according NodeDecorator, EdgeDecorator, LabelDecorator, PortDecorator, and BendDecorator should be used. All those decorators provide a getDecoratorFor<TInterface> method that returns a LookupDecorator<TDecoratedType,TInterface> that in turn provides convenient ways to decorate the specified interface.
To decorate other items implementing ILookup, retrieve the ILookupDecorator via lookup and create a new instance of LookupDecorator<TDecoratedType,TInterface> for the interface that shall be decorated.
Examples for Lookup Decoration
To illustrate the use of lookups in customization, we will provide examples for the usages of these lookups in the customization process. For a start, we will begin with an INodeSizeConstraintProvider, for which an INode’s lookup is queried during interactive node resizing.
Let’s assume that we want to create a custom INodeSizeConstraintProvider and place it into the lookup of all nodes. In the first example, we use the getDecoratorFor<TInterface> method of NodeDecorator.
// first, get the decorator for nodes.
const decorator = graph.decorator.nodes
// create a lookup decorator for the INodeSizeConstraintProvider interface
const lookupDecorator = decorator.getDecoratorFor(INodeSizeConstraintProvider)
// use the convenience method addFactory to add a lookup to the lookup chain
// which returns a new INodeSizeConstraintProvider instance when
// the INodeSizeConstraintProvider interface is queried for a node.
lookupDecorator.addFactory((node) => {
const size = node.layout.toSize()
return new NodeSizeConstraintProvider(size.multiply(0.5), size.multiply(2))
})
The code example above uses the getDecoratorFor<TInterface> method for demonstration purposes. The same result could be achieved using the more straightforward NodeDecorator.sizeConstraintProvider.
The first step is to obtain the NodeDecorator from the GraphDecorator. Once we have the NodeDecorator, we use its getDecoratorFor<TInterface> method to receive a LookupDecorator<TDecoratedType,TInterface> for the INodeSizeConstraintProvider type we want to return our new constraint provider for.
In this example, we want to restrict node resizing to half the node’s original size at minimum and twice its original size as maximum. For this use case, we need the current node to retrieve its original size, so we use the addFactory(Factory<TDecoratedType, TInterface>) method to have the current node available.
As a second example, let’s assume our application has one instance of ITable that is of importance, and it should be made available via our IGraph. As IGraph implements ILookup, we can lookup its ILookupDecorator as shown in Creating a LookupDecorator
// first, get the decorator for the graph's lookup.
const decorator = graph.lookup(ILookupDecorator)
// now create a new LookupDecorator for item type IGraph and interface type ITable
// that uses this decorator
const lookupDecorator = new LookupDecorator(IGraph, ITable, decorator)
// finally decorate the lookup with our (constant) table instance
lookupDecorator.addConstant(table)
// first, get the decorator for the graph's lookup.
const decorator = graph.lookup(ILookupDecorator)!
// now create a new LookupDecorator for item type IGraph and interface type ITable
// that uses this decorator
const lookupDecorator = new LookupDecorator(IGraph, ITable, decorator)
// finally decorate the lookup with our (constant) table instance
lookupDecorator.addConstant(table)
Now we can use any of LookupDecorator<TDecoratedType,TInterface>s convenience methods, and as we have a single table that we want to provide, we just go with the addConstant(TInterface) method.
Another common use case is removing existing items from the lookup. If you added them yourself, you can store a reference to the IContextLookupChainLink and call ILookupDecorator.removeLookup later on, as demonstrated in Removing an added chain link.
const decorator = graph.lookup(ILookupDecorator)
const lookupDecorator = new LookupDecorator(IGraph, ITable, decorator)
// store chain link returned by the convenience method
const chainLink = lookupDecorator.addConstant(table)
// this chain link can now be removed from the ILookupDecorator
decorator.removeLookup(IGraph, chainLink)
const decorator = graph.lookup(ILookupDecorator)!
const lookupDecorator = new LookupDecorator(IGraph, ITable, decorator)
// store chain link returned by the convenience method
const chainLink = lookupDecorator.addConstant(table)
// this chain link can now be removed from the ILookupDecorator
decorator.removeLookup(IGraph, chainLink)
However, most of the time you don’t have the exact lookup chain link at hand
that is responsible for returning a specific object when queried.
In those cases, you can use the LookupDecorator<TDecoratedType, TInterface>.hide method of an appropriate LookupDecorator<TDecoratedType,TInterface> that effectively
hides the rest of the chain by returning null
.
The Hiding lookups example shows this approach. In the example, hiding is used to remove the visual appearance for selected edges. For information on the visualization of selection, focus, and highlighting, have a look at the Model Manager section.
const edgeDecorator = graph.decorator.edges
// read as: add a lookup to the lookup chain for IEdges that returns null
// whenever an ISelectionRenderer is queried,
// thus ending the chain and hiding whatever the lookup may return when looking further.
edgeDecorator.selectionRenderer.hide()