Skip to content

FSM Editor Programming Discussion

Gary edited this page Mar 10, 2015 · 2 revisions

Table of Contents

The ATF Fsm Editor Sample shows how to use the ATF graph facilities to create a finite state machine (FSM) editor, so it has some similarities to the ATF Circuit Editor Sample, which is described in Circuit Editor Programming Discussion. You can drag states from a palette onto a canvas and then connect them with transitions. You can also drag comment windows onto the canvas to enter explanatory text. You can copy items and paste them onto the Prototypes window for later reuse by dragging them back onto the canvas.

A fully functional state machine tool based on ATF has been developed.

Programming Overview

FsmEditor is a graph editor, so it employs the various ATF graph handling mechanisms; for details see Graphs in ATF. ATF does not offer special support for the simple graphs that FsmEditor uses, so see General Graph Support as well.

The data model is quite simple with no extension of data types and only a few data types to describe the limited number of state machine items.

Most of the classes in FsmEditor are DOM adapters, and they handle graph adaptation, documents, contexts, and graph validation.

The Editor component provides state machine display and editing, setting up the D2dAdaptableControl in which the state machine appears. It sets up control adapters for the D2dAdaptableControl in a very similar fashion to the ATF Circuit Editor Sample. You can also print state machines from the canvas, handled by the PrintableDocument class.

Contexts in FsmEditor handle editing contexts in the canvas as well as the Prototypes window. Prototypes are handled almost identically to the ATF Circuit Editor Sample. The ViewingContext provides viewing functions for the finite state machine on the canvas and also enables printing it.

Validation of the state machine is primarily to ensure than transition arrows between the same pairs of states don't overlap and is handled by TransitionRouter.

Graphs and State Machines

A state machine is a graph, and so this sample uses the graphical classes in the Sce.Atf.Controls.Adaptable.Graphs namespace.

In the most general form, ATF supports graphs using nodes, edges, and routes in its IGraph<IGraphNode, IGraphEdge<IGraphNode, IEdgeRoute>, IEdgeRoute> interface:

public interface IGraph<out TNode, out TEdge, out TEdgeRoute>
    where TNode : class, IGraphNode
    where TEdge : class, IGraphEdge<TNode, TEdgeRoute>
    where TEdgeRoute : class, IEdgeRoute

where the elements are:

  • IGraphNode: Interface for a node in a graph; nodes are connected by edges.
  • IGraphEdge<TNode, TEdgeRoute>: Interface for routed edges that connect nodes and have a defined source and destination route from and to the nodes.
  • IEdgeRoute: Interface for edge routes, which act as sources and destinations for graph edges.
The FsmEditor's Fsm class, derived from DomNodeAdapter, uses its own IGraph variant:
public class Fsm : DomNodeAdapter, IGraph<State, Transition, NumberedRoute>, IAnnotatedDiagram

The parameters in the IGraph<State, Transition, NumberedRoute> interface are:

  • State: DOM adapter for states.
  • Transition: DOM adapter for transitions in the state machine.
  • NumberedRoute: Route for directed graph edges, so that multiple edges do not overlap on the canvas.
For more information about IGraph and other graph interfaces, see ATF Graph Interfaces. For details on the Fsm class, see Fsm Class Graph Adapter.

FsmEditor Data Model

Like most of the samples, this one defines a data model using an XML Schema. The main design is in the file FSM.xsd, which defines these types:

  • stateType: a state in the state machine.
  • transitionType: a transition from one state to another.
  • annotationType: a comment.
  • prototypeType: a set of states and transitions.
  • prototypeFolderType: a collection of prototypes and prototype folders.
  • fsmType: the state machine itself, consisting of all its states, transitions, annotations, and prototype folders.
FSM.xsd is augmented by the file FSM_customized.xsd, which adds extra attributes for state and transition types using a redefine element.

This schema is simple; no types are based on other types. Some types can have child types. In this figure from the Visual Studio XML Schema Explorer of the schema files, the parent types' nodes are opened to show their child types:

FsmEditor uses the GenSchemaDef.bat command file for DomGen to create the Schema class (containing type metadata classes) from the XML Schema files.

FsmEditor has a SchemaLoader class derived from XmlSchemaTypeLoader as usual. Its overridden OnSchemaSetLoaded() also performs the typical tasks of associating data with the Schema class's metadata classes:

FsmEditor DOM Adapters

Many of the classes in FsmEditor are DOM adapters, and they perform a wide variety of functions, including handling basic types, providing contexts, and displaying and editing state machines.

FsmType DOM Adapters

Most of the DOM adapters are defined for the "fsmType" type, which represents the whole state machine. The root element of the DomNode tree is of "fsmType". Such adapters fall in these general categories:

FsmEditor Types DOM Adapters

Each of the types in the data model, such as "stateType" and "transitionType", has a DOM adapter defined for it.

Annotation DOM Adapter

The adapter Annotation implements IAnnotation, the interface for an annotation on a diagram. It is similar to the Sce.Atf.Controls.Adaptable.Graphs.Annotation class used by ATF Circuit Editor Sample.

Annotation defines properties for the annotation attributes that are saved in the application data:

  • Text: Get or set annotation text as a string.
  • Location: Get or set annotation center point as a Point. The rectangle size does not persist; the rectangle is sized to fit the data.
Only the Text property is in IAnnotation. Note that Text implements a setter, even though this is not in IAnnotation.

IAnnotation also has a Bounds property to set annotation bounds and a SetTextSize() method to set the size of the annotation's text, measured using the annotation font. The Bounds property gets and sets Location for its own get and set.

Its OnNodeSet() method sets the comment text to the default value "Comment", so that appears in the comment when you drag a Comment item from the palette onto the canvas:

protected override void OnNodeSet()
{
    base.OnNodeSet();
    if (string.IsNullOrEmpty(Text))
        Text = "Comment";
}

The text is set only when there is no text already set.

Prototype and PrototypeFolder DOM Adapters

These adapters simply get and set attributes of the Schema metadata classes for these two types. For instance, Prototype gets and sets the prototype name with the DomNode.GetAttribute() and DomNode.SetAttribute() methods. Prototype's States and Transitions properties gets a list of states and transitions, using the DomNodeAdapter.GetChildList() method.

PrototypeFolder folder does something similar with its Folders and Prototypes properties.

These DOM adapters are used in the prototyping context. For details, see PrototypingContext Class.

State DOM Adapter

State implements IGraphNode, a simple interface with the properties Name and Bounds for the name and bounding rectangle of the state. In addition, State has properties whose values are attributes of "stateType", so they are accessed with the DomNode.GetAttribute() and DomNode.SetAttribute() methods:

  • Position: Get or set center Point of state.
  • Size: Get or set diameter of state circle.
  • Hidden: Get or set visibility of state.

Transition DOM Adapter

A transition connects states directionally, so it is a graph edge, and thus Transition implements IGraphEdge<State, NumberedRoute>, the interface for routed edges in a graph. This interface in turn implements IGraphEdge<State>.

Transition also implements the application data properties FromState, ToState, and Label, which simply get and set the appropriate attributes for "stateType". IGraphEdge<State> contains the properties FromNode, ToNode, and Label. These properties simply get the value of the FromState, ToState, and Label properties.

IGraphEdge<State, NumberedRoute> has properties for the routes' "from" and "to" states FromRoute and ToRoute, which both get the same NumberedRoute object.

Fsm Class Graph Adapter

The Fsm class is a DOM adapter for the entire state machine and adapts the state machine to a graph. It is defined on "fsmType", which is the type of the root DomNode of the tree representing the state machine. As previously mentioned, it implements IGraph<State, Transition, NumberedRoute> and IAnnotatedDiagram.

IGraph's properties, Nodes and Edges, get lists of these items. Fsm has properties that get an IList of various state machine entities: States, Transitions, and Annotations.

IAnnotatedDiagram simply gets a list of annotations, which Fsm provides with its Annotations property.

For more information about IGraph and other graph interfaces, see ATF Graph Interfaces.

FsmEditor Documents

FsmEditor implements both document and document client interfaces. For a general discussion of documents, see Implementing a Document and Its Client.

Document Class

Document, a DOM adapter defined for the "fsmType", derives from DomDocument that implements IDocument.

Document is a minimal class, simply providing the document Type and handling URI and dirty changed events.

Editor Component

Editor is both the document client and control host client for the control that holds the state machine graph, so it implements IDocumentClient and IControlHostClient.

Document Client

Much of what Document does as a document client is common to other samples and is well described in Implementing a Document and Its Client.

The main method of interest here is Open(), which creates a D2dAdaptableControl to display the state machine graph. The process of creating and setting up control adapters for the D2dAdaptableControl has much in common with the ATF Circuit Editor Sample and is discussed in Circuit Document Display and Control Adapters.

Adapters

A few control adapters deserve special mention:

var fsmAdapter = // adapt control to allow binding to graph data
    new D2dGraphAdapter<State, Transition, NumberedRoute>(m_fsmRenderer, transformAdapter);

var fsmStateEditAdapter = // adapt control to allow state editing
    new D2dGraphNodeEditAdapter<State, Transition, NumberedRoute>(m_fsmRenderer, fsmAdapter, transformAdapter);

var fsmTransitionEditAdapter = // adapt control to allow transition
    new D2dGraphEdgeEditAdapter<State, Transition, NumberedRoute>(m_fsmRenderer, fsmAdapter, transformAdapter);

var mouseLayoutManipulator = new MouseLayoutManipulator(transformAdapter);

Note that several of these adapters take a renderer m_fsmRenderer, which is constructed in this way:

m_theme = new D2dDiagramTheme();
m_fsmRenderer = new D2dDigraphRenderer<State, Transition>(m_theme);

D2dDigraphRenderer is a general graph rendering class; for more information, see Direct2D Renderers. It takes a D2dDiagramTheme, a diagram rendering theme class for Direct2D rendering. Its constructor configures a variety of graphic objects to set the theme for drawing the graph:

public D2dDiagramTheme(string fontFamilyName, float fontSize)
{
    m_d2dTextFormat = D2dFactory.CreateTextFormat(fontFamilyName, fontSize);
    m_fillBrush = D2dFactory.CreateSolidBrush(SystemColors.Window);
    m_textBrush = D2dFactory.CreateSolidBrush(SystemColors.WindowText);
    m_outlineBrush = D2dFactory.CreateSolidBrush(SystemColors.ControlDark);
    ...
    int fontHeight = (int)TextFormat.FontHeight;
    m_rowSpacing = fontHeight + PinMargin;
    m_pinOffset = (fontHeight - m_pinSize) / 2;

    D2dGradientStop[] gradstops =
    {
        new D2dGradientStop(Color.White, 0),
        new D2dGradientStop(Color.LightSteelBlue, 1.0f),
    };
    m_fillLinearGradientBrush = D2dFactory.CreateLinearGradientBrush(gradstops);
    StrokeWidth = 2;
}

Getting back to the control adapters, D2dGraphAdapter adapts a control to displaying a graph. For more information on this adapter, see D2dGraphAdapter Class and Control Adapters.

D2dGraphNodeEditAdapter adds graph node dragging capabilities to an adapted control. In FsmEditor, this allows you to reposition states on the canvas by dragging them. For more details on this adapter, see D2dGraphNodeEditAdapter Class and Control Adapters.

D2dGraphEdgeEditAdapter provides graph edge dragging capabilities to an adapted control. For more details on this adapter, see D2dGraphEdgeEditAdapter Class and Control Adapters.

The ATF Circuit Editor Sample also uses these adapters, and its usage is described in Circuit Document Display and Control Adapters.

MouseLayoutManipulator marks a state as selected. For more information, see Control Adapters.

HoverAdapter is used to display information about states when the cursor hovers over them. For a discussion of how to use this adapter, see Hover Adapter.

Finishing Up

After calling control.Adapt() to set all the control adapters, Open() performs a few housekeeping tasks:

// associate the control with the viewing context; other adapters use this
//  adapter for viewing, layout and calculating bounds.
ViewingContext viewingContext = node.Cast<ViewingContext>();
viewingContext.Control = control;

// set document URI
document = node.As<Document>();
ControlInfo controlInfo = new ControlInfo(fileName, filePath, StandardControlGroup.Center);

//Set IsDocument to true to prevent exception in command service if two files with the
//  same name, but in different directories, are opened.
controlInfo.IsDocument = true;

document.ControlInfo = controlInfo;
document.Uri = uri;

// now that the data is complete, initialize the rest of the extensions to the Dom data;
//  this is needed for adapters such as validators, which may not be referenced anywhere
//  but still need to be initialized.
node.InitializeExtensions();

// set control's context to main editing context
EditingContext editingContext = node.Cast<EditingContext>();
control.Context = editingContext;

These final tasks consist of adapting the root DomNode to various objects. The root DomNode is of type "fsmType", and so it can be adapted to ViewingContext, Document, and EditingContext, because all of these DOM adapters are defined on "fsmType".

The adapted objects are used to store information. For example, the adapted ViewingContext's Control property is set to the D2dAdaptableControl. The adapted Document has its ControlInfo and Uri properties set. And the D2dAdaptableControl's Context property is set to the adapted EditingContext.

The last thing the Open() method does is to register the D2dAdaptableControl it just constructed with the control host service:

m_controlHostService.RegisterControl(control, controlInfo, this);

Control Host Client

This client also behaves very similarly to other samples' control host clients. Its Activate() method sets the active context and document. Close() tries to close the active document and gives the user a chance to save it if it has changed.

For further discussion, see Creating Control Clients.

PrintableDocument Class

PrintableDocument confers the ability to print the canvas with its implementation of IPrintableDocument. The IPrintableDocument.GetPrintDocument() gets a PrintDocument that can be printed and works with the standard Windows® print dialogs.

This PrintDocument object is actually a FsmPrintDocument deriving from CanvasPrintDocument, the abstract base class for printing a canvas. FsmPrintDocument overrides methods to get "page" bounds for the various PrintRange values, as well as the Render() method to actually render the canvas to a printed page:

protected override void Render(RectangleF sourceBounds, Matrix transform, Graphics g)
{
    g.SmoothingMode = SmoothingMode.AntiAlias;
    g.InterpolationMode = InterpolationMode.HighQualityBicubic;

    g.Transform = transform;
    sourceBounds.Inflate(1, 1); // allow for pen widths
    g.SetClip(sourceBounds);

    foreach (IPrintingAdapter printingAdapter in m_viewingContext.Control.AsAll<IPrintingAdapter>())
        printingAdapter.Print(this, g);
}

The printing operation is done by the ViewingContext (in m_viewingContext) that is adapted to an IPrintingAdapter, which has a Print() method to render the canvas to a GDI Graphics object. For the discussion of FsmEditor's ViewingContext, see ViewingContext Class.

FsmEditor Contexts

FMSEditor context classes provide several different kinds of capabilities.

EditingContext Class

This context governs editing the state machine on the canvas. Like several samples, the EditingContext derives from Sce.Atf.Dom.EditingContext:

public class EditingContext : Sce.Atf.Dom.EditingContext,
    IEnumerableContext,
    IObservableContext,
    INamingContext,
    IInstancingContext,
    IEditableGraph<State, Transition, NumberedRoute>

There may be several of these EditingContexts in a document, and PrototypingContext also derives from EditingContext. For more information on this useful context, see Sce.Atf.Dom.EditingContext Class.

There are several interfaces implemented that many other editing contexts in the samples implement:

  • IEnumerableContext: Enumerate items in the state machine.
  • IObservableContext: Events for states being added or removed. This is required so the state machine can be refreshed after it changes.
  • INamingContext: Name states.
  • IInstancingContext: Handle copy and paste of selected state and transition items.
For an example of how circuit graphs use interfaces like this, see CircuitEditingContext Class.

IInstancingContext requires the most work to implement. Some of the methods in this interface are very similar to other IInstancingContext implementations. Both CanCopy() and CanDelete(), for instance, simply check that Selection.Count > 0, that is, that something is selected. Copy() iterates through the selection, copying whichever DomNodes are adapted to State, Transition, and Annotation. Insert() uses a DragDropAdapter to find the insertion location for the object. It then copies the list of inserted objects and adapts it to an enumeration of DomNodes, which are then adapted to an appropriate list, such as List<State>, and added to the list of such items in the state machine.

EditingContext also uses the interface IEditableGraph<State, Transition, NumberedRoute> to edit transitions between states. This interface includes methods to determine whether connections can be made or undone, and to make or break the connection: CanConnect, Connect, CanDisconnect, and Disconnect. Making a transition means adding a "transitionType" object, which adds a DomNode of that type to the DomNode tree:

Transition IEditableGraph<State, Transition, NumberedRoute>.Connect(
    State fromNode, NumberedRoute fromRoute, State toNode, NumberedRoute toRoute, Transition existingEdge)
{
    DomNode domNode = new DomNode(Schema.transitionType.Type);
    Transition transition = domNode.As<Transition>();

    transition.FromState = fromNode as State;
    transition.ToState = toNode as State;
    // we set the route after the logical operation completes

    if (existingEdge != null)
        transition.Label = existingEdge.Label;

    m_fsm.Transitions.Add(transition);
    return transition;
}

Note that the new Transition object, a DomNode adapted to Transition, has its FromState and ToState properties set to the appropriate DomNodes adapted to State.

PrototypingContext Class

This context is for editing the Protoypes window's prototype list, which is created by the PrototypeLister component. This component requires an IPrototypingContext to be viewed and edited by the user, so PrototypingContext implements IPrototypingContext.

Like EditingContext, PrototypingContext derives from Sce.Atf.Dom.EditingContext. PrototypingContext also implements several of the interfaces that EditingContext does, such as IObservableContext, and they serve much the same purposes, although in the context of a prototype list.

IPrototypingContext itself requires ITreeView and IItemView, so PrototypingContext implements them as well. For more information on these interfaces, see ITreeView Interface and IItemView Interface.

Because PrototypingContext is a separate context from EditingContext and also that the MultipleHistoryContext DOM adapter for multiple history contexts is defined for the root type "fsmType", the undo/redo history stacks for these two contexts is distinct. That is, you can undo and redo on the main canvas and the Prototypes window separately.

This context is almost identical to Sce.Atf.Controls.Adaptable.Graphs.PrototypingContext. For more information, see PrototypingContext Class.

ViewingContext Class

ViewingContext is a DOM adapter that provides viewing functions for the finite state machine on the canvas. It contains a reference to the D2dAdaptableControl so it can update the control's canvas bounds during validation. PrintableDocument also requires a ViewingContext. It implements ILayoutContext and IViewingContext:

public class ViewingContext : Validator, ILayoutContext, IViewingContext

IViewingContext allows framing items in the canvas and ensuring their visibility.

ILayoutContext works with the bounds of canvas items. GetBounds(), for instance, attempts to adapt the item as either a State or Annotation and, if successful, returns the Bounds property:

BoundsSpecified ILayoutContext.GetBounds(object item, out Rectangle bounds)
{
    State state = Adapters.As<State>(item);
    if (state != null)
    {
        bounds = state.Bounds;
        return BoundsSpecified.All;
    }

    Annotation annotation = Adapters.As<Annotation>(item);
    if (annotation != null)
    {
        bounds = annotation.Bounds;
        return BoundsSpecified.All;
    }

    bounds = new Rectangle();
    return BoundsSpecified.None;
}

A Transition is not bounded, per se, because a transition's location is determined by its "from" and "to" states. Drawing it requires no bounds information.

Both the IViewingContext and ILayoutContext interfaces are discussed further in Context Interfaces.

Validating State Machines

The SchemaLoader defines a few validating DOM adapters on the "fsmType" type:

Schema.fsmType.Type.Define(new ExtensionInfo<TransitionRouter>());
...
Schema.fsmType.Type.Define(new ExtensionInfo<ReferenceValidator>());
Schema.fsmType.Type.Define(new ExtensionInfo<UniqueIdValidator>());

UniqueIdValidator Class

The UniqueIdValidator DOM adapter ensures that every DOM node in the subtree has a unique ID. The "stateType" has an ID ("xs:ID") that "transitionType" references for the "from" and "to" states, as seen in the XML Schema definition:

<xs:complexType name="stateType">
  <xs:attribute name="name" type="xs:ID" use="required" />
  <xs:attribute name="label" type="xs:string" />
  <xs:attribute name="x" type="xs:int" use="required" />
  <xs:attribute name="y" type="xs:int" use="required" />
  <xs:attribute name="size" type="xs:int" />
  <xs:attribute name="hidden" type="xs:boolean" />
  <xs:attribute name="start" type="xs:boolean" />
</xs:complexType>
...
<xs:complexType name="transitionType">
  <xs:attribute name="label" type="xs:string" />
  <xs:attribute name="source" type="xs:IDREF" use="required" />
  <xs:attribute name="destination" type="xs:IDREF" use="required" />
</xs:complexType>

These IDs must therefore be checked for uniqueness.

ReferenceValidator Class

As just seen, the type "transitionType" references IDs for the "from" and "to" states in the "transitionType", so these references must be validated whenever the graph changes. ReferenceValidator tracks references and reference holders to ensure reference integrity within DOM data. Checks are only made within validations, which are signaled by IValidationContexts. This adapter should be defined on the DOM's root node type, and it is: on "fsmType". This adapter does the following:

  1. Track all DomNode references in the subtree.
  2. Raise notifications if external DomNode references are added or removed.
  3. After validating, raise notification events if referents have been removed, leaving dangling references.

TransitionRouter Class

This validator is summoned when making transitions. Its purpose is to track changes to transitions and update their routing, so that the transition arrows don't overlap on the canvas.

When a transition is drawn and the user releases the mouse, OnEnded() is called, which calls RouteTransitions():

private void RouteTransitions()
{
    Dictionary<Pair<State, State>, int> routesByPair = new Dictionary<Pair<State, State>, int>();
    foreach (Transition transition in m_fsm.Transitions)
    {
        Pair<State, State> states = new Pair<State, State>(transition.FromState, transition.ToState);
        int routes;
        if (!routesByPair.TryGetValue(states, out routes))
        {
            // start routes at 0 if there are no connections in the opposite direction,
            //  otherwise, start at 1
            if (routesByPair.ContainsKey(new Pair<State, State>(transition.ToState, transition.FromState)))
                routes = 1;
            routesByPair.Add(states, routes);
        }

        transition.Route = routes++;
        routesByPair[states] = routes;
    }
}

This method creates a dictionary of all the transition from-to state pairs as the key, with the number of routes between this pair as the value. The dictionary is created while iterating through all the transitions in the state machine. Each transition's Route property is set each loop iteration so it is set to a unique value:

transition.Route = routes++;

This allows the NumberedRoutes to be set up so the transition arrows don't overlap.

Palette Operations

FsmEditor creates and uses a palette with its PaletteClient class and palette data set up, described in FsmEditor Data Model. Palette implementation is very similar to the ATF Simple DOM Editor Sample. For details on setting up a palette, see Using a Palette in Simple DOM Editor Programming Discussion.

Topics in this section

Clone this wiki locally