Skip to content

Using Dom Programming Discussion

Gary edited this page Mar 10, 2015 · 2 revisions

Table of Contents

The ATF Using Dom Sample is a simple and direct example of how to use the ATF DOM in an application. It contains an XML Schema, a schema metadata class file generated by DomGen, DOM adapters for types, and a schema loader. It also shows saving application data in an XML file.

This sample application has no UI. You can run it in a Windows® Command Prompt to see its output text. It saves the application data it creates in an XML file game.xml.

Programming Overview

UsingDom has no UI and does not use MEF, and it mainly demonstrates how to create and handle application data with the ATF DOM.

It has a very simple data model with one base type that game objects are based on, and the model is defined in the XML Schema file game.xsd. Similarly to many of the other samples, UsingDom has a metadata class created by DomGen. It uses a simple schema loader based on XmlSchemaTypeLoader that mainly loads the schema and defines DOM adapters on data types. These DOM adapters primarily define properties for type attributes.

The Main() function loads the XML Schema, which also results in defining the DOM adapters. It creates the application data by building a tree of DomNodes and setting their attributes. It shows two ways to do this, one by directly setting DomNode attributes and the other by setting DOM adapted object's properties. This first method is sufficient, so this sample does not even need to define DOM adapters.

The sample also prints out the application data to a Windows® Command Prompt.

UsingDom persists its application data to an XML file with the DomXmlWriter class, which uses the loaded XML Schema.

UsingDom Data Model

UsingDom specifies its data model in the XML Schema file game.xsd. The "gameObjectType" is for game objects, and the other game objects are based on it: "dwarfType", "ogreType", and "treeType". The type "gameType" is the root type and contains "gameObjectType" type objects:

<xs:complexType name="gameType">
  <xs:sequence>
    <xs:element name="gameObject" type="gameObjectType" minOccurs="0" maxOccurs="unbounded" />
  </xs:sequence>
  <xs:attribute name="name" type="xs:ID"/>
</xs:complexType>
...
<xs:element name="game" type="gameType"/>

The entire data schema is shown in this view of the Visual Studio XML Schema Explorer, with all types opened to show their attributes and contained objects:

Note that "treeType" has no attributes of its own, only the attributes of its base type "gameObjectType":

<xs:complexType name="treeType">
  <xs:complexContent>
    <xs:extension base="gameObjectType">
    </xs:extension>
  </xs:complexContent>
</xs:complexType>

This shows that a derived type does not have to define additional attributes. This definition does accomplish making "treeType" a "gameObjectType", and that's its only purpose. This allows "treeType" objects to be contained in the "gameType" object.

The command file GenSchemaDef.bat runs DomGen on the XML Schema file game.xsd:

{source} ..\..\..\DevTools\DomGen\bin\DomGen.exe game.xsd GameSchema.cs Game.UsingDom UsingDom {source}

DomGen creates the metadata class file GameSchema.cs defining the class GameSchema, which shows how the metadata classes are constructed. For instance, here's what is generated from the schema definition for the root type "gameType", shown previously:

public static class gameType
{
    public static DomNodeType Type;
    public static AttributeInfo nameAttribute;
    public static ChildInfo gameObjectChild;
}

The three class members are the following:

  • Type: DomNodeType for the type of the DomNode associated with an object of "gameType".
  • nameAttribute: AttributeInfo object for the attribute "name".
  • gameObjectChild: ChildInfo object for the list of objects this type can contain, of type "gameObjectType".
The GameSchema class's Initialize() method initializes the gameType class this way:
gameType.Type = typeCollection.GetNodeType("gameType");
gameType.nameAttribute = gameType.Type.GetAttributeInfo("name");
gameType.gameObjectChild = gameType.Type.GetChildInfo("gameObject");

This accomplishes the following:

  • Defines the node type Type using the XmlSchemaTypeCollection.GetNodeType() method to the type of "gameType".
  • Sets nameAttribute with DomNodeType.GetAttributeInfo() to the attribute type for the "name" attribute.
  • Initializes gameObjectChild using the DomNodeType.GetChildInfo() method to the child info for "gameObject" elements, which are the objects that the gameType object can contain.

UsingDom Schema Loader

The GameSchemaLoader class derives from XmlSchemaTypeLoader, the ATF base schema loader class. Its constructor loads the schema file:

public GameSchemaLoader()
{
    // set resolver to locate embedded .xsd file
    SchemaResolver = new ResourceStreamResolver(Assembly.GetExecutingAssembly(), "UsingDom/Schemas");
    Load("game.xsd");
}

The XmlSchemaTypeLoader.Load() method calls OnSchemaSetLoaded() after the schema is loaded to perform any desired initialization:

protected override void OnSchemaSetLoaded(XmlSchemaSet schemaSet)
{
    foreach (XmlSchemaTypeCollection typeCollection in GetTypeCollections())
    {
        m_namespace = typeCollection.TargetNamespace;
        m_typeCollection = typeCollection;
        GameSchema.Initialize(typeCollection);

        // register extensions
        GameSchema.gameType.Type.Define(new ExtensionInfo<Game>());
        GameSchema.gameType.Type.Define(new ExtensionInfo<ReferenceValidator>());
        GameSchema.gameType.Type.Define(new ExtensionInfo<UniqueIdValidator>());

        GameSchema.gameObjectType.Type.Define(new ExtensionInfo<GameObject>());
        GameSchema.dwarfType.Type.Define(new ExtensionInfo<Dwarf>());
        GameSchema.ogreType.Type.Define(new ExtensionInfo<Ogre>());
        break;
    }
}

The foreach executes only once, because there's only one collection of types here. After setting field values used by a couple of properties, NameSpace and TypeCollection, the loop defines DOM adapters.

The first three are for the root type "gameType", so they apply to the root DomNode:

  • Game is for the type "gameType" itself.
  • UniqueIdValidator makes sure that IDs for nodes are unique. Note that the schema for "gameType" specified an ID for its "name" attribute: <xs:attribute name="name" type="xs:ID"/>.
  • ReferenceValidator verifies that references in the DOM data are valid. This is usually used for references to IDs, and is not needed in this sample, because there are no data references in the data model.
The last three DOM adapters are defined for each of the game object types that have attributes. There is no DOM adapter for "treeType", because it has only the attributes from its base type, "gameObjectType", which does have a DOM adapter.

For details on what all the DOM adapters do, see DOM Adapters in UsingDom.

DOM Adapters in UsingDom

A DOM adapter class provides the capabilities desired for a DomNode that can be adapted to the DOM adapter. In UsingDom, the DOM adapters provide properties for each attribute of the type the DOM adapter is defined for.

For example, here's the DOM adapter GameObject for the "gameObjectType" type, which is about as simple as it gets:

public class GameObject : DomNodeAdapter
{
    /// <summary>
    /// Gets or sets game object name</summary>
    public string Name
    {
        get { return GetAttribute<string>(GameSchema.gameObjectType.nameAttribute); }
        set { SetAttribute(GameSchema.gameObjectType.nameAttribute, value); }
    }
}

The "gameObjectType" type has just one attribute, "name". GameObject's Name property uses the DomNodeAdapter methods GetAttribute() and SetAttribute() on the nameAttribute for the property's getter and setter. Recall that nameAttribute is defined as an AttributeInfo in GameSchema:

public static AttributeInfo nameAttribute;

And DomNodeAdapter.GetAttribute() takes an AttributeInfo parameter:

protected T GetAttribute<T>(AttributeInfo attributeInfo)

Name's property definition is typical for attribute properties in the sample applications that use the ATF DOM.

The DOM adapter Game defined for "gameType" also defines a property for its "name" attribute, which is the name of the game. It also defines the GameObjects property for the game objects that an object of this type contains:

public class Game : DomNodeAdapter
{
    /// <summary>
    /// Performs initialization when the adapter is connected to the game's DomNode.
    /// Raises the DomNodeAdapter NodeSet event and performs custom processing.</summary>
    protected override void OnNodeSet()
    {
        base.OnNodeSet();
        m_gameObjects = GetChildList<GameObject>(GameSchema.gameType.gameObjectChild);
    }

    /// <summary>
    /// Gets or sets the game's name</summary>
    public string Name
    {
        get { return GetAttribute<string>(GameSchema.gameType.nameAttribute); }
        set { SetAttribute(GameSchema.gameType.nameAttribute, value); }
    }

    /// <summary>
    /// Gets a list of the game objects</summary>
    public IList<GameObject> GameObjects
    {
        get { return m_gameObjects; }
    }

    private IList<GameObject> m_gameObjects;
}

GameObjects simply returns the value in the m_gameObjects field for getting its value. This DOM adapter also has an OnNodeSet() method, which sets the value of m_gameObjects. The DomNodeAdapter.GetChildList() method used here takes a ChildInfo parameter:

protected IList<T> GetChildList<T>(ChildInfo childInfo)

Thus getting the value of GameObjects returns an IList<GameObject>: a list of the DOM adapter objects for the game objects contained in the DomNode. DOM adapters adapt DomNodes, so this is essentially a list of the child DomNodes of the "gameType" node, the root DomNode.

The DOM adapters Dwarf and Ogre both derive from the GameObject DOM adapter, because their types are based on "gameObjectType":

public class Dwarf : GameObject

In general, if data type A is based on type B, the DOM adapter defined on A should derive from the DOM adapter defined on the type B.

UsingDom Main Function

Main() puts all the pieces together.

Schema Loader Invocation

Main() creates a schema loader:

var gameSchemaLoader = new GameSchemaLoader();

As noted in UsingDom Schema Loader, GameSchemaLoader's constructor loads the schema, and that also calls OnSchemaSetLoaded() that defines the DOM adapters, so the application can use them.

Creating Application Data

UsingDom offers two ways to create application data, one of which is commented out:

DomNode game = null;
// create game either using DomNode or DomNodeAdapter.
game = CreateGameUsingDomNode();
//game = CreateGameUsingDomNodeAdapter();

The DomNode game is the root DomNode for the tree of DomNodes that contain the application data. UsingDom creates application data by creating a tree of DomNodes and putting data into each DomNode by setting its attributes. The code here allows for two approaches to setting up the DomNodes:

  • CreateGameUsingDomNode(): Create DomNodes and set their attributes directly.
  • CreateGameUsingDomNodeAdapter(): Create DomNodes and set their attributes by using DOM adapters on the nodes.
These approaches offer an interesting contrast. First consider how these different methods set up the root DomNode. CreateGameUsingDomNode() does it this way:
// create Dom node of the root type defined by the schema
DomNode game = new DomNode(GameSchema.gameType.Type, GameSchema.gameRootElement);
game.SetAttribute(GameSchema.gameType.nameAttribute, "Ogre Adventure II");
IList<DomNode> childList = game.GetChildList(GameSchema.gameType.gameObjectChild);

This method creates a new DomNode of the type GameSchema.gameType.Type with ChildInfo from GameSchema.gameRootElement, using this version of the DomNode constructor:

public DomNode(DomNodeType nodeType, ChildInfo childInfo)

Next, it sets the "name" attribute with the DomNode.SetAttribute() method. (Note that the DOM adapters described in DOM Adapters in UsingDom set attributes this way.) Finally, it gets a list of the root's child DomNodes with the DomNode.GetChildList() method (just as the DOM adapter did).

Contrast this to the approach of CreateGameUsingDomNodeAdapter(), which simply sets properties of the DOM adapter:

DomNode root = new DomNode(GameSchema.gameType.Type, GameSchema.gameRootElement);
Game game = root.As<Game>();
game.Name = "Ogre Adventure II";

It creates the root DomNode exactly the same way. However, it then initializes a Game object by adapting the root DomNode to the Game DOM adapter with the Adapters.As() method:

public static T As<T>(this IAdaptable adaptable)

This adaptation works, because DomNode implements IAdaptable:

public sealed class DomNode : IAdaptable, IDecoratable

The last line sets the Name property to "Ogre Adventure II". By the definition of the Game DOM adapter, setting this property sets the "name" attribute of its associated DomNode to the given value. So the result is exactly the same for both methods.

It is not necessary to get the list of the root's child DomNodes as in the previous approach, because this list is already available in the Game DOM adapter's property Game.GameObjects, as previously shown in DOM Adapters in UsingDom.

Although both approaches accomplish the same result, using DOM adapters makes the code simpler and easier to read.

The "ogreType" is also handled differently in the two methods. Here's CreateGameUsingDomNode():

// Add an ogre
DomNode ogre = new DomNode(GameSchema.ogreType.Type);
ogre.SetAttribute(GameSchema.ogreType.nameAttribute, "Bill");
ogre.SetAttribute(GameSchema.ogreType.sizeAttribute, 12);
ogre.SetAttribute(GameSchema.ogreType.strengthAttribute, 100);
childList.Add(ogre);

As before, this sets the ogre's attributes using the DomNode.SetAttribute() method. At the end, it adds the new DomNode as a child of the root DomNode using the childList obtained earlier from the root DomNode.

Here's CreateGameUsingDomNodeAdapter()'s approach to doing the same thing:

// Add an ogre
DomNode ogreNode = new DomNode(GameSchema.ogreType.Type);
Ogre orge = ogreNode.As<Ogre>();
orge.Name = "Bill";
orge.Size = 12;
orge.Strength = 100;
game.GameObjects.Add(orge);

Again, it creates the ogre DomNode exactly the same as in CreateGameUsingDomNode(). However, it then adapts this node to an Ogre DOM adapter object. It sets attribute values using Ogre's Name, Size, and Strength properties. The Name property actually belongs to the GameObject DOM adapter that Ogre derives from:

public class Ogre : GameObject

The last difference is that the new DomNode is added as a child to the GameObjects property of the Game object game, because this property holds the list of child DomNodes.

At its end, CreateGameUsingDomNode() returns the DomNode it created in the beginning:

return game;

On the other hand, CreateGameUsingDomNodeAdapter() returns the DomNodeAdapter.DomNode property of the Game DOM adapter, which contains the adapted DomNode:

return game.DomNode;

The CreateGameUsingDomNode() method creates application data without DOM adapters, so the DOM adapters are not really required, except to illustrate this alternate approach to setting up data. However, using DOM adapters makes the code simpler and easier to read.

Printing Application Data

After application data is created, it is printed:

Print(game);

The Print() method prints a subtree of DomNodes:

private static void Print(DomNode game)
{
    Console.WriteLine("Game: {0}", game.GetAttribute(game.Type.GetAttributeInfo("name")));

    foreach (DomNode child in game.Children)
    {
        Console.WriteLine();
        Console.WriteLine("   {0}", child.Type.Name);
        foreach (AttributeInfo attr in child.Type.Attributes)
            Console.WriteLine("      {0}: {1}",
                    attr.Name,
                    child.GetAttribute(attr));
    }
    Console.WriteLine();
}

The first WriteLine() call prints data from the root DomNode's attributes obtained with DomNodeType.GetAttribute(). The subsequent outer loop prints information on each child of the root, obtained from the child list in the DomNode.Children property. The inner loop iterates through all the attributes of each child, obtained from the list of attributes in the DomNodeType.Attributes property.

This is the output you get when running UsingDom in a Windows® Command Prompt:

{source} Game.UsingDom:ogreType

      name: Bill
      size: 12
      strength: 100
   Game.UsingDom:dwarfType
      name: Sally
      age: 32
      experience: 55
   Game.UsingDom:treeType
      name: Mr. Oak

{source}

Saving Application Data

It's easy to persist application data when you use the ATF DOM and an XML Schema for type definitions. The last section of Main() sets up the file path for the saved data file and then saves the data:

// create directory for data files
Directory.CreateDirectory(Path.Combine(ExecutablePath, @"data"));
string filePath = Path.Combine(ExecutablePath, "data\\game.xml");
var gameUri = new Uri(filePath);

// save game.
FileMode fileMode = FileMode.Create;
using (FileStream stream = new FileStream(filePath, fileMode))
{
    DomXmlWriter writer = new DomXmlWriter(gameSchemaLoader.TypeCollection);
    writer.Write(game, stream, gameUri);
}

The most interesting part is the ATF specific line that creates a DomXmlWriter using this constructor:

public DomXmlWriter(XmlSchemaTypeCollection typeCollection)

The constructor needs an XmlSchemaTypeCollection, which contains the loaded schema — all the data model's definitions, plus names that can be used in the XML. The DomXmlWriter.Write() method writes the node tree, given its root, to the given stream and URI:

public virtual void Write(DomNode root, Stream stream, Uri uri)

The resulting file game.xml looks like this:

<?xml version="1.0" encoding="utf-8"?>
<game xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xs="http://www.w3.org/2001/XMLSchema" name="Ogre Adventure II" xmlns="Game.UsingDom">
    <gameObject xsi:type="ogreType" name="Bill" size="12" strength="100" />
    <gameObject xsi:type="dwarfType" name="Sally" age="32" experience="55" />
    <gameObject xsi:type="treeType" name="Mr. Oak" />
</game>

First, notice that the XML tag names for the objects, <game> and <gameObject>, are the element names of the root type and the items contained in a "gameType" object.

Next, the XML attribute names are the attribute names defined in the types. For example, the <game> tag, of type "gameType", has the attribute "name", which is defined in the XML Schema. The first <gameObject> tag has an "xsi:type" of "ogreType", which is the type name of an ogre. Its other attributes, "name", "size", and "strength", come from the type definitions for "gameObjectType" and "ogreType".

In other words, the XML in the Schema nicely parallels the XML in the saved file.

Topics in this section

Clone this wiki locally