Skip to content

DOM Tree Editor Programming Discussion

Gary edited this page Mar 10, 2015 · 3 revisions

Table of Contents

The ATF DOM Tree Editor Sample is an editor for UI elements, which are hierarchical and displayed as a tree in the main editing window. The data is handled by the ATF DOM. The application's data types' attributes are designed with enough variety to demonstrate the various value editors and controls ATF offers. For more information on value editors, see Property Editing in ATF.

The sample also illustrates the CurveEditor component, which allows you to draw curves in a window. For more information on using it, see CurveEditor Component.

Programming Overview

This sample defines its data model in an XML Schema and uses the ATF DOM to handle application data, including persisting data. Containment relationships are an important part of the data model and affect how the document can be edited.

The data model also includes specifying property descriptors for type attributes, so they can be viewed and edited in property editors. Descriptors are specified in both annotations in the XML Schema and in descriptor constructors in the schema loader. Using annotations requires special support from the schema loader to parse the annotations.

This sample's data types have diverse attribute types, so a plethora of value editors are used to display them in the property editors.

Over half the classes in this sample are DOM adapters, most for the various data types. DOM adapters are also used for documents and data verification.

The sample uses its TreeLister and TreeView component and class to display and edit its data. The EditingContext class provides the context for editing data, as in drag and drop operations from the UI element palette. The palette's implementation closely resembles other samples.

DOMTreeEditor Data Model

The data model uses the ATF DOM. It also defines property descriptors for its data types' attributes, so the attributes appear properly in property editors. The GenSchemaDef.bat command file runs DomGen to generate the UISchema.cs file containing the UISchema class with its metadata classes.

DOMTreeEditor Data Definition

DOMTreeEditor defines its types in an XML Schema type definition file UISchema.xsd for UI element types. This file includes CurveSchema.xsd, which defines the control point and curve types for curves in the CurveEditor window.

The base type is "UIObjectType" representing UI elements; most of the other types are based on "UIObjectType", including the root type "UIType". The root type is the type of the root DomNode in the tree. Most of the objects, such as animations and textures, are of "UIObjectType". A couple types, "UISpriteType" and "UITextItemType", are of type "UIControlType" — also based on "UIObjectType". This figure from the Visual Studio XML Schema Explorer shows the type base relations:

This figure also shows the containment relationships: all the object types that can contain objects of other types have their nodes opened to show their child types. For instance, the root type "UIType" can contain an object of "UIPackageType", which can contain objects of types "UIFormType", "UIShaderType", "UITextureType", and "UIFontType". These relationships are enforced by DOMTreeEditor when you drag objects from the palette onto other objects, as described in Context Handling. The first object you can drag onto the editing window is a package, for example, because the root type "UIType" only contains "UIPackageType".

Property Descriptor Annotations

Some of the attributes in the schema definition have scea.dom.editors.attribute annotations to define property descriptors:

<!--The values here are ignored, but need enumeration type for UISpriteType-->
<xs:simpleType name="spriteEnumType">
  <xs:restriction base="xs:string">
    <xs:enumeration value="xxxx"/>
  </xs:restriction>
</xs:simpleType>

<xs:complexType name="UISpriteType">
  <xs:annotation>
    <xs:appinfo>
      <scea.dom.editors.attribute name="SpriteType" displayName="Sprite type" description="Sprite type"
        editor="Sce.Atf.Controls.PropertyEditing.LongEnumEditor,Atf.Gui.WinForms:Pixie,Ghost,Gollum,Robot,Skeleton,Big bird"
        converter="Sce.Atf.Controls.PropertyEditing.EnumTypeConverter" />
    </xs:appinfo>
  </xs:annotation>
  <xs:complexContent>
    <xs:extension base="UIControlType">
      <xs:sequence>
        <xs:element name="Shader" type="UIRefType" minOccurs="0" maxOccurs="1"/>
      </xs:sequence>
      <xs:attribute name="SpriteType" type="spriteEnumType"/>
    </xs:extension>
  </xs:complexContent>
</xs:complexType>

This annotation defines a property descriptor for the "SpriteType" attribute of the type "UISpriteType". It specifies LongEnumEditor as the value editor for this attribute, which has the type "spriteEnumType", shown just before the "UISpriteType" definition. This attribute must have an enumeration type to be able to use LongEnumEditor as its value editor. The "SpriteType" attribute has a stub enumeration definition; the actual enumeration values are listed as parameters with the value editor specification. LongEnumEditor implements IAnnotatedParams, so it takes a list of enumeration value parameters. Note also that the value editor's path name is fully qualified, and its assembly "Atf.Gui.WinForms" is also spelled out. Specifying the wrong path or assembly results in the schema not being loaded, because the value editor can't be located. The annotation also gives the value converter EnumTypeConverter that converts the parameters to the enumeration's values, and this converter is required for LongEnumEditor.

For more details on specifying attribute property descriptors in annotations, see the ATF Programmer's Guide: Document Object Model (DOM), which can be downloaded at ATF Documentation.

Schema Loader

This application's schema loader is similar to other samples. As usual, it derives from XmlSchemaTypeLoader to do most of the schema loading work.

Its OnSchemaSetLoaded() method performs these typical functions after the schema is loaded:

Type Tag Property Descriptors

The schema loader also sets tags to a PropertyDescriptorCollection value on types to define their property descriptors. The descriptors defined for "UIControlType" have a number of interesting features:

UISchema.UIControlType.Type.SetTag(
    new PropertyDescriptorCollection(
        new PropertyDescriptor[]
    {
        new ChildAttributePropertyDescriptor(
            Localizer.Localize("Translation"),
            UISchema.UITransformType.TranslateAttribute,
            UISchema.UIControlType.TransformChild,
            null,
            Localizer.Localize("Item position"),
            false,
            new NumericTupleEditor(typeof(float), new string[] { "X", "Y", "Z" }),
            new FloatArrayConverter()),
        new ChildAttributePropertyDescriptor(
            Localizer.Localize("Rotation"),
            UISchema.UITransformType.RotateAttribute,
            UISchema.UIControlType.TransformChild,
            null,
            Localizer.Localize("Item rotation"),
            false,
            new NumericTupleEditor(typeof(float), new string[] { "X", "Y", "Z" }),
            new FloatArrayConverter()),
        new ChildAttributePropertyDescriptor(
            Localizer.Localize("Scale"),
            UISchema.UITransformType.ScaleAttribute,
            UISchema.UIControlType.TransformChild,
            null,
            Localizer.Localize("Item scale"),
            false,
            new UniformArrayEditor<float>())
    }));

The PropertyDescriptorCollection contains ChildAttributePropertyDescriptors for several attributes. Note that ChildAttributePropertyDescriptor is used, because these descriptors are for attributes of "UITransformType", which is a type of a child node of "UIControlType" — not for attributes of "UIControlType" itself. The descriptors here describe the attributes of "UITransformType": "RotateAttribute", "ScaleAttribute", and "TranslateAttribute". A "UIControlType" object can have children of types "UITransformType", "UIAnimationType", and "UIControlType", and so a property editor for an object of any of these types would show the "RotateAttribute", "ScaleAttribute", and "TranslateAttribute" properties. A property descriptor for a type attribute that is an attribute of "UIControlType" would use an AttributePropertyDescriptor constructor instead.

Note that the ChildAttributePropertyDescriptor constructors contain the same kind of information as the property descriptor annotations described in Property Descriptor Annotations, because they are both specifying the same thing.

"TranslateAttribute" and "RotateAttribute" both use a NumericTupleEditor value editor, because they both are of type "vector3Type", a vector of 3 float values. The NumericTupleEditor constructor takes two values for the data type and the names of the numeric fields:

public NumericTupleEditor(Type numericType, string[] names)

This descriptor definition also contains the associated value converter FloatArrayConverter, required by NumericTupleEditor.

Although the attribute "ScaleAttribute" also has the type "vector3Type", it uses a different value editor UniformArrayEditor. This editor also handles an array of numbers, but only one value is exposed in the value editing control and all the array's numbers are set to this value. This forces the scaling of all three dimensions to be identical for this property. This demonstrates that while an attribute must use a value editor appropriate for its value type, it is not limited to only one choice.

For further discussion of these two ways of specifying property descriptors and what is required, see Property Descriptors in DOMTreeEditor.

DOMTreeEditor DOM Adapters

The classes whose names begin with "UI" — almost half the classes in DOMTreeEditor — are DOM adapters. Most are very simple, merely providing properties for their attributes. For instance, the base class UIObject only describes a Name property:

/// <summary>
/// Base for all UI DomNode adapters, with a name attribute</summary>
public abstract class UIObject : DomNodeAdapter
{
    /// <summary>
    /// Gets or sets the UI object's name</summary>
    public string Name
    {
        get { return GetAttribute<string>(UISchema.UIObjectType.nameAttribute); }
        set { SetAttribute(UISchema.UIObjectType.nameAttribute, value); }
    }
}

The "Name" attribute is common to all types. The class UIControl is also fairly simple, deriving from UIObject:

public class UIControl : UIObject
{
    /// <summary>
    /// Performs initialization when the adapter is connected to the diagram annotation's DomNode</summary>
    protected override void OnNodeSet()
    {
        DomNode transform = DomNode.GetChild(UISchema.UIControlType.TransformChild);
        if (transform == null)
        {
            transform = new DomNode(UISchema.UITransformType.Type);
            transform.SetAttribute(UISchema.UITransformType.ScaleAttribute, new float[] { 1.0f, 1.0f, 1.0f });
            DomNode.SetChild(UISchema.UIControlType.TransformChild, transform);
        }
    }

    /// <summary>
    /// Gets the control's transform</summary>
    public UITransform Transform
    {
        get { return GetChild<UITransform>(UISchema.UIControlType.TransformChild); }
    }

    /// <summary>
    /// Gets the list of all child controls in the control</summary>
    public IList<UIControl> Controls
    {
        get { return GetChildList<UIControl>(UISchema.UIControlType.ControlChild); }
    }
}

According to its definition, "UIControlType" can have an unlimited number of "UIControlType" children, so its Controls property returns a list of these children:

<xs:complexType name="UIControlType" abstract="true">
  <xs:complexContent>
    <xs:extension base="UIObjectType">
      <xs:sequence>
        <xs:element name="Transform" type="UITransformType" minOccurs="1" maxOccurs="1"/>
        <xs:element name ="Animation" type="UIAnimationType" minOccurs="0" maxOccurs="1"/>
        <xs:element name="Control" type="UIControlType" minOccurs="0" maxOccurs="unbounded"/>
      </xs:sequence>
    </xs:extension>
  </xs:complexContent>
</xs:complexType>

It has only one "UITransformType" child, which its Transform property returns.

DOM adapters do not necessarily define properties for all the type's attributes' values. If the value is not used programmatically, then no property is needed. For example, though "UITextItemType" has a "text" attribute, there is no property to obtain its value in the UITextItem DOM adapter.

Most of the DOM adapters don't even have OnNodeSet() methods, although UIControl does. Its main task is to set the scale attribute's values for the transform associated with the control object, if the transform is not already set:

transform.SetAttribute(UISchema.UITransformType.ScaleAttribute, new float[] { 1.0f, 1.0f, 1.0f });

Because of this, whenever a control object, Sprite or Text, is dragged onto the editor window, it already has its scale values set.

The other two DOM adapters are ControlPoint and Curve. These adapters are primarily devoted to implementing IControlPoint and ICurve respectively. IControlPoint is used by ICurve, and the CurveEditor component manipulates ICurve objects, which represent curves. Inside DOMTreeEditor, Curve is used by UIAnimation whose OnNodeSet() method creates a set of curves that are displayed in the Curve Editor window when an Animation object is selected.

The SchemaLoader defines DOM adapters for their corresponding types:

// register adapters to define the UI object model
UISchema.UIPackageType.Type.Define(new ExtensionInfo<UIPackage>());
UISchema.UIFormType.Type.Define(new ExtensionInfo<UIForm>());
UISchema.UIShaderType.Type.Define(new ExtensionInfo<UIShader>());
UISchema.UITextureType.Type.Define(new ExtensionInfo<UITexture>());
UISchema.UIFontType.Type.Define(new ExtensionInfo<UIFont>());
UISchema.UISpriteType.Type.Define(new ExtensionInfo<UISprite>());
UISchema.UITextItemType.Type.Define(new ExtensionInfo<UITextItem>());
UISchema.UIRefType.Type.Define(new ExtensionInfo<UIRef>());
UISchema.UIAnimationType.Type.Define(new ExtensionInfo<UIAnimation>());
UISchema.curveType.Type.Define(new ExtensionInfo<Curve>());
UISchema.controlPointType.Type.Define(new ExtensionInfo<ControlPoint>());

Palette Implementation

DOMTreeEditor creates and uses a palette with its PaletteClient class and palette data set up, described in Schema Loader. 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.

Document Handling

This sample's document handling is straightforward and in the fashion of several other samples. Its Document class derives from DomDocument, a DOM adapter that implements IDocument. Its Editor component implements IDocumentClient to handle opening, saving, and closing documents, using XML to persist documents.

Document is defined as the DOM adapter for "UIType", the type of the DomNode tree:

UISchema.UIType.Type.Define(new ExtensionInfo<Document>());

For general information about creating documents, see Implementing a Document and Its Client. More specifically, the ATF Simple DOM Editor Sample handles documents much the same as this sample. For details, see Document Handling in Simple DOM Editor Programming Discussion.

Tree Handling with TreeLister and TreeView

The editing window holding the current document displays its contents as a tree using the TreeLister component, which derives from TreeControlEditor, the base class for tree editors. Only one document can be open at a time.

TreeView is the DOM adapter for the root DomNode in the tree of application data. Similarly to other samples such as ATF File Explorer Sample and ATF Circuit Editor Sample, it implements ITreeView, IItemView, and IObservableContext so the data in the tree can be displayed by TreeLister:

  • ITreeView: Define a view on hierarchical data so it can be displayed in a tree.
  • IItemView: Provide display information about an item in an ItemInfo object. ItemInfo holds information on the appearance and behavior of an item in a list or tree control.
  • IObservableContext: Its events trigger refreshing the editor window display.
TreeView is defined as the DOM adapter for "UIType", the type of the DomNode tree root:
UISchema.UIType.Type.Define(new ExtensionInfo<TreeView>());

For information on how other samples and classes use these interfaces to display data in a tree, see File Explorer Programming Discussion and Circuit Graph Support.

TreeLister is the control host for the tree control, implementing IControlHostClient. Its Activate() method sets the active context to the TreeView property, which is the ITreeView object displayed by the editor. Its Close() method is invoked when the application ends and closes the open document, if any. The OnLastHitChanged() method performs the important function of setting the last object in the tree a palette item was dragged over and the mouse button released, which is the potential insertion parent for the palette item.

The TreeControlEditor handles drag and drop operations in the tree, in this case, dragging and dropping objects from the palette onto their new parent objects.

Context Handling

The EditingContext class derives from Sce.Atf.Dom.EditingContext, which is used as an editing context in several samples, such as ATF FSM Editor Sample, ATF Simple DOM Editor Sample, and ATF State Chart Editor Sample. For more information on this useful context, see Sce.Atf.Dom.EditingContext Class.

EditingContext is defined as the DOM adapter for "UIType", the type of the DomNode tree:

UISchema.UIType.Type.Define(new ExtensionInfo<EditingContext>());

EditingContext's main job is implementing IInstancingContext to create UI element instances when items are dragged from the palette onto the tree control. Its methods are called during instancing operations — dragging and dropping of palette items onto the tree. For a discussion of instancing, see Instancing In ATF.

EditingContext must enforce the parenting relationships discussed in DOMTreeEditor Data Definition. Several types of objects have child objects, but only of certain types.

Here, the key method is IInstancingContext.CanInsert(), which is called when a palette item is dragged over an object in the tree and the mouse button released. CanInsert() determines whether the item can be inserted at the insertion point, which is the last object the item was dragged over and the mouse button released. This insertion point is set by SetInsertionParent(), which was called by TreeLister.OnLastHitChanged(). CanInsert() checks whether the insertion object is suitable by calling CanParent():

private bool CanParent(DomNode parent, DomNodeType childType)
{
    return GetChildInfo(parent, childType) != null;
}

GetChildInfo() checks if the type of the potentially inserted item is one of the child types under the parent. If not, it returns null, so the insertion fails:

private ChildInfo GetChildInfo(DomNode parent, DomNodeType childType)
{
    foreach (ChildInfo childInfo in parent.Type.Children)
        if (childInfo.Type.IsAssignableFrom(childType))
            return childInfo;
    return null;
}

In addition, the control objects, Sprite and Text, can take a reference to a Shader or Font object, respectively, when the right kind of object is dragged to the empty reference "slot". The EmptyRef class represents the empty slot for a reference before it is set. The CanReference() method checks the validity of these kind of insertions by checking that the right kind of object is dropped over its parent:

private bool CanReference(EmptyRef emptyRef, DomNodeType childType)
{
    return
        // dropping shader on sprite?
        ((childType == UISchema.UIShaderType.Type) &&
        emptyRef.ChildInfo.IsEquivalent(UISchema.UISpriteType.ShaderChild)) ||

        // dropping font on text item?
        ((childType == UISchema.UIFontType.Type) &&
        emptyRef.ChildInfo.IsEquivalent(UISchema.UITextItemType.FontChild));
}

Finally, the IInstancingContext.Insert() method performs the actual insertion: creating new DomNodes, initializing their extensions (DOM adapters), and adding them as children to the parent DomNode. It must also handle references appropriately for Sprite and Text parents.

Validation in DOMTreeEditor

This sample uses several validators to ensure the integrity of the application data by examining the DomNode tree is response to various events.

The Validator class is particular to DOMTreeEditor and checks that resources that are referenced are available in the same package, responding to DomNodes being inserted in the tree. Validator derives from Sce.Atf.Dom.Validator, the abstract base class for validators that need to track all validation events in a subtree.

DOMTreeEditor employs a couple other general purpose validators to maintain DomNode tree integrity, and these are all defined for the type of the root DomNode "UIType", so the entire tree is checked:

UISchema.UIType.Type.Define(new ExtensionInfo<Validator>()); // makes sure referenced resources are in package
                                                             // this must be first so unique naming can work on copied resources
UISchema.UIType.Type.Define(new ExtensionInfo<ReferenceValidator>()); // prevents dangling references
UISchema.UIType.Type.Define(new ExtensionInfo<UniqueIdValidator>());  // makes sure ref targets have unique ids

The other validators do the following:

  • ReferenceValidator: Track references and reference holders to ensure integrity of references within the DOM data.
  • UniqueIdValidator: Ensure that every DomNode in a subtree has a unique ID.
For a general discussion of validators, see the ATF Programmer's Guide: Document Object Model (DOM).

Property Descriptors in DOMTreeEditor

To display property attributes of application data in property editors, you must specify a property descriptor for every attribute you want to view and edit. Property descriptors hold such information as the property metadata attribute, the user interface name, and a value editor to display the value in its associated value editing control. For details on specifying properties, see Property Editing in ATF.

DOMTreeEditor offers a rich variety of types in its data model to demonstrate the various value editors and their controls. For instance, this figure shows texture properties, its attributes taking on several different types:

This shows several value editors:

  • Texture file: FileUriEditor to select a URI for the texture file.
  • Texture folder: FolderBrowserDialogUITypeEditor to select texture folder.
  • Texture numbers: ArrayEditor to specify an array of numbers.
  • Texture revision date: DateTimeEditor to set the revision date.
For a description and illustrations of all ATF value editors, see Value Editors and Value Editing Controls.

Specifying Property Descriptors

As previously mentioned, you can describe property descriptors two ways in ATF: by an XML Schema annotation or by a property descriptor constructor, usually in the schema loader. DOMTreeEditor employs both methods.

XML Schema Annotation Property Descriptors

Here is the type definition for the "UIShaderType" from the type definition file UISchema.xsd:

<xs:complexType name="UIShaderType">
  <xs:annotation>
    <xs:appinfo>
      <scea.dom.editors.attribute name="ShaderID" displayName="Shader ID" description="Shader ID"
        editor="Sce.Atf.Controls.PropertyEditing.BoundedIntEditor,Atf.Gui.WinForms:10,1000"
        converter="Sce.Atf.Controls.PropertyEditing.BoundedIntConverter" />
    </xs:appinfo>
  </xs:annotation>
  <xs:complexContent>
    <xs:extension base="UIObjectType">
      <xs:attribute name="FxFile" type="xs:string" use="required"/>
      <xs:attribute name="ShaderID" type="xs:int" use="required"/>
      <xs:attribute name="ShaderParam" type="xs:int" use="required" />
    </xs:extension>
  </xs:complexContent>
</xs:complexType>

The annotation for the Shader ID attribute specifies:

  • Name
  • Display name and description
  • Value editor BoundedIntEditor
  • Value converter BoundedIntConverter
The paths for the value editor BoundedIntEditor and converter BoundedIntConverter are fully qualified, plus the assembly for the editor. The editor is also followed by parameters indicating its lower and upper limits.

Note that this annotation only specifies the property descriptor for one of the three attributes of this property. The rest are specified in property descriptor constructors, described in the next section.

To process annotations in the XML Schema, you must provide override the base XmlSchemaTypeLoader.ParseAnnotations() method, which is described in ParseAnnotations Method.

Property Descriptor Constructors

The remaining property descriptors for the "UIShaderType" type's attributes are in SchemaLoader.OnSchemaSetLoaded():

UISchema.UIShaderType.Type.SetTag(
    new PropertyDescriptorCollection(
        new PropertyDescriptor[] {
        new AttributePropertyDescriptor(
            Localizer.Localize("Shader"),
            UISchema.UIShaderType.FxFileAttribute,
            null,
            Localizer.Localize("Shader file path"),
            false,
            new FileUriEditor("Fx Files (*.fx)|*.fx")),
        new AttributePropertyDescriptor(
            Localizer.Localize("Shader param"),
            UISchema.UIShaderType.ShaderParamAttribute,
            null,
            Localizer.Localize("Shader param"),
            false,
            new NumericEditor(typeof(Int32)))
    }));

The call to NamedMetadata.SetTag() sets a tag for the UIShaderType attribute consisting of a PropertyDescriptorCollection containing an array of AttributePropertyDescriptors. The constructor for each AttributePropertyDescriptor contains much the same information as the annotation:

public AttributePropertyDescriptor(
    string name,
    AttributeInfo attribute,
    string category,
    string description,
    bool isReadOnly,
    object editor)

Constructors are provided for the value editors FileUriEditor and NumericEditor used to edit the attributes in property editors.

ParseAnnotations Method

ParseAnnotations() parses annotations in XML schemas to create property descriptors from information in annotations. This method is overridden here, because the base method does not call PropertyDescriptor.ParseXml(), which processes <scea.dom.editors.attribute> annotations for property descriptors. This override does call ParseXml():

protected override void ParseAnnotations(
    XmlSchemaSet schemaSet,
    IDictionary<NamedMetadata, IList<XmlNode>> annotations)
{
    base.ParseAnnotations(schemaSet, annotations);

    IList<XmlNode> xmlNodes;

    foreach (DomNodeType nodeType in m_typeCollection.GetNodeTypes())
    {
        // Parse XML annotation for property descriptors
        if (annotations.TryGetValue(nodeType, out xmlNodes))
        {
            PropertyDescriptorCollection propertyDescriptors =
                Sce.Atf.Dom.PropertyDescriptor.ParseXml(nodeType, xmlNodes);
            if (propertyDescriptors != null && propertyDescriptors.Count > 0)
            {
                // Property descriptor annotation found. Add any descriptors already set for this type.
                PropertyDescriptorCollection propertyDescriptorsAlreadySet =
                    nodeType.GetTag<PropertyDescriptorCollection>();
                if (propertyDescriptorsAlreadySet != null)
                    foreach (PropertyDescriptor desc in propertyDescriptorsAlreadySet)
                        propertyDescriptors.Add(desc);
                // Set all property descriptors
                nodeType.SetTag<PropertyDescriptorCollection>(propertyDescriptors);
            }
        }
    }
}

The method XmlSchemaTypeLoader.Load() loads the schema from the type definition file. In Load(), OnSchemaSetLoaded() is called first, followed by ParseAnnotations(). OnSchemaSetLoaded() is where property descriptor constructors are processed, and property descriptor annotations get handled in ParseAnnotations().

The inner loop in ParseAnnotations() must do special processing to ensure that property descriptors specified by both an annotation and a property descriptor constructor are both added to the PropertyDescriptorCollection collection for the property. Otherwise, the property descriptors set in OnSchemaSetLoaded() would be overwritten by the ones ParseAnnotations() sets. If a property descriptor was found in the annotation, ParseAnnotations() gets any property descriptors already found for the type and adds them to the PropertyDescriptorCollection for the type.

For more information on parsing annotations, see the ATF Programmer's Guide: Document Object Model (DOM).

Topics in this section

Clone this wiki locally