Skip to content

DOM Property Editor Programming Discussion

Gary edited this page Mar 10, 2015 · 3 revisions

Table of Contents

Data items in an application can have various attributes/properties. The ATF DOMPropertyEditor Sample shows how to use the ATF DOM with an XML Schema to define application data with attributes, whose values can be viewed and edited using the ATF PropertyEditor component. The data model has a large variety of attribute data types, so the sample uses various value editing controls in the property descriptors it uses to specify which attributes to view and edit.

Note that property in the context of this sample refers to an attribute of an application data type — not to be confused with a C# property.

For general information on editing properties, see Property Editing in ATF.

Programming Overview

The DOMPropertyEditor sample defines a simple data model with objects that can contain collections of other objects. The Editor component creates an initial set of data objects, which can be expanded by adding armor, weapons, and orc child objects.

The most interesting part of the sample is its SchemaLoader.OnSchemaSetLoaded() method that defines property descriptors to indicate which attributes show up in property editors. The objects' attributes show a wide variety of data types, so various value editors are specified in the property descriptors, appropriate to the attribute's data type. The EmbeddedCollectionEditor is an embedded property editor for adding, removing, and editing items in a collection, and this sample uses it for the collection of objects an orc object can have: armor, weapons, and children. The sample provides EmbeddedCollectionEditor methods to allow you to add, remove, and move items in the collections.

DOMPropertyEditor Data Model

This sample has a simple data model, described in this figure from the Visual Studio XML Schema Explorer for the game.xsd type definition file:

The figure shows all attributes and containment relationships of all the types.

The root type is "gameType", and the root object can contain any number of "gameObjectType" objects. However, "gameObjectType" is an abstract type, and the data model has one type that extends it: "orcType". In turn, an "orcType" object can contain any number of "clubType", "armorType", or other "orcType" objects as children. Because this sample uses the ATF DOM, this means, for instance, that the root DomNode of "gameType" has children DomNodes of "gameObjectType" in the DomNode tree holding application data.

This sample uses DomGen to generate a Schema class from the type definition file, similarly to other samples that use the DOM. This class contains metadata objects for all the types in the data model. For more information, see DomNodes and DOM Metadata Classes.

DOM Adapters in DOMPropertyEditor

DOM Adapters play a minor role in this sample. As usual, the schema loader class SchemaLoader's OnSchemaSetLoaded() method defines DOM adapters:

Schema.gameType.Type.Define(new ExtensionInfo<GameEditingContext>());
Schema.gameType.Type.Define(new ExtensionInfo<UniqueIdValidator>());

UniqueIdValidator is the ATF DOM validator that checks for unique DomNode IDs. The GameEditingContext DOM adapter derives from the general purpose editing context EditingContext, which derives from HistoryContext. This allows the sample to undo and redo editing operations. For details on this useful context, see Sce.Atf.Dom.EditingContext Class.

The GameEditingContext's OnNodeSet() method subscribes to the DomNode change events AttributeChanged, ChildInserted, and ChildRemoved when the root DomNode is initialized. It does this so that change events can be recorded in the history context, so undo/redo works. If these events were not subscribed to, changes to application data could not be undone or redone.

Editor Component

The Editor component has only one function: to create application data. Its IInitializable.Initialize() method is called after all components are initialized. This method creates a delegate that runs when the application's main form first displays:

// create root node.
var rootNode = new DomNode(Schema.gameType.Type, Schema.gameRootElement);
rootNode.SetAttribute(Schema.gameType.nameAttribute, "Game");

// create Orc game object and add it to rootNode.
var orc = CreateOrc();
rootNode.GetChildList(Schema.gameType.gameObjectChild).Add(orc);

// add a child Orc.
var orcChildList = orc.GetChildList(Schema.orcType.orcChild);
orcChildList.Add(CreateOrc("Child Orc1"));

rootNode.InitializeExtensions();

// set active context and select orc object.
m_contextRegistry.ActiveContext = rootNode;
var edContext = rootNode.Cast<GameEditingContext>();
edContext.Set(orc);

This code gets type information from the Schema class's metadata subclasses to create the right type for the root DomNode. It also sets the node's "nameAttribute" AttributeInfo by calling the DomNode.SetAttribute() method. It goes on to create an "orcType" object with the CreateOrc() method, which creates a new DomNode of "orcType" and sets object attributes in a similar fashion as just seen.

Both these methods also call DomNode.GetChildList() to get the list of child DomNodes of the object. This is an IList<DomNode>, so you can invoke Add() on it to add a new child to the list.

The method calls DomNode.InitializeExtensions() on the root DomNode to initialize all the DOM adapters defined for the type of the root DomNode, which is the only type for which DOM adapters are defined, as noted previously in DOM Adapters in DOMPropertyEditor.

The CreateArmor() and CreateClub() methods create "armorType" and "clubType" objects to add as children to "orcType" objects. It does this by creating DomNode objects of the appropriate type and then adding them to the child lists of similar objects in the "orcType" object.

Defining Property Descriptors for Attributes

The sample imports the PropertyEditor component to display application data attributes, aka, properties. The heart of this sample is the portion of the SchemaLoader.OnSchemaSetLoaded() method that defines property descriptors. These descriptors specify exactly what displays in this property editor — and how it is displayed. The descriptors specify different property value editors to display attribute data in a variety of ways, suitable for the type of data. For more information on value editors, see Value Editors and Value Editors and Value Editing Controls.

This sample specifies property descriptors using constructors, rather than annotations in the type definition XML Schema. The advantage of this approach is that property descriptor data is checked at compile time. On the other hand, annotations allow you to keep all the type information in one place and does not require recompiling the application for changes to take effect. For a discussion of property descriptors and the ways of specifying them, see DOM Property Descriptors.

Property Descriptor Usage

This sample lays out property descriptors a little differently than most of the other samples. For example, the ATF DOM Tree Editor Sample nests AttributePropertyDescriptor constructors inside a PropertyDescriptorCollection constructor, as shown in Specifying Property Descriptors in Constructors. This construction may be a bit to take in at first sight. The DOM Property Editing sample constructs these objects separately:

// Descriptors for armorType.
string general = "General".Localize();
var armorDescriptors = new PropertyDescriptorCollection(null);
armorDescriptors.Add(new AttributePropertyDescriptor(
    "Name".Localize(),
    Schema.armorType.nameAttribute,
    general,
    "Armor name".Localize(),
    false
    ));

armorDescriptors.Add(new AttributePropertyDescriptor(
    "Defense".Localize(),
    Schema.armorType.defenseAttribute,
    general,
    "Armor defense".Localize(),
    false,
    new NumericEditor(typeof(int))
    ));

armorDescriptors.Add(new AttributePropertyDescriptor(
    "Price".Localize(),
    Schema.armorType.priceAttribute,
    general,
    "Armor price in gold".Localize(),
    false,
    new NumericEditor(typeof(int))
    ));

Schema.armorType.Type.SetTag(armorDescriptors);

This code segment first creates an empty PropertyDescriptorCollection. It then constructs AttributePropertyDescriptors for the "name", "defense", and "price" attributes of "armorType" and adds them to the PropertyDescriptorCollection. Finally, it calls NamedMetadata.SetTag<T>() to add this property descriptor information to the DomNodeType object Schema.armorType.Type from the Schema class.

Note that several different forms of constructors are used for AttributePropertyDescriptor. The first one has this form:

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

The last two constructors are similar, but add an editor object:

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

The "name" attribute is displayed in the default value editor for a string object, whereas the "defense" and "price" attributes appear in a NumericEditor value editor. In general, you can specify how you want to view an attribute's value by selecting the value editor.

The result of this code sequence is that the "name", "defense", and "price" attributes are displayed in the property editor for "armorType" objects, as will be seen in the EmbeddedCollectionEditor Class section. The sample specifies descriptors in a similar fashion to display "clubType" object attributes. The BoolEditor value editor is used to display the "spikes" attribute of a "clubType" object.

Game and Orc Object Descriptors

Property descriptors are specified for all "gameObjectType" attributes, so all are viewed in the property editor. Here is the XML Schema specification for "gameObjectType":

<xs:complexType name="gameObjectType" abstract="true">
  <xs:attribute name="name" type="xs:ID"/>
  <xs:attribute name="visible" type="xs:boolean" default="true" />
  <xs:attribute name="translate" type="vector3Type" default="0 0 0"/>
  <xs:attribute name="rotate" type="vector3Type" default="0 0 0"/>
  <xs:attribute name="scale" type="vector3Type" default="1 1 1"/>
</xs:complexType>

Several of these attributes have the type "vector3Type" of three floating point numbers. It's interesting to see how their property descriptors are set up. Here's the descriptor for the "translate" attribute:

// NumericTupleEditor can be used for vector values.
string xformCategory = "Transformation".Localize();
var transEditor =
    new NumericTupleEditor(typeof(float), new string[] { "Tx", "Ty", "Tz" });

gobDescriptors.Add(
    new AttributePropertyDescriptor(
            "Translate".Localize(),
            Schema.gameObjectType.translateAttribute,
            xformCategory,
            "Object's position".Localize(),
            false,
            transEditor
            ));

First, the string xformCategory is used to specify a category for the attribute. And a NumericTupleEditor is used for the value editor. In the property editor, these attributes are grouped under the category "Transformation" and displayed as specified in the NumericTupleEditor:

The "translate" attribute's value editor has "Tx", "Ty", and "Tz" labels, specified in the NumericTupleEditor constructor.

Various property editors are used for "orcType" attributes. For instance, the "goals" attribute uses FlagsTypeConverter:

string chCategory = "Character attributes".Localize();
...
FlagsUITypeEditor goalsEditor = new FlagsUITypeEditor(Enum.GetNames(typeof(OrcGoals)));
FlagsTypeConverter goalsConverter = new FlagsTypeConverter(Enum.GetNames(typeof(OrcGoals)));
orcDescriptors.Add(
    new AttributePropertyDescriptor(
        "Goals".Localize(),
        Schema.orcType.goalsAttribute,
        chCategory,
        "Goals".Localize(),
        false,
        goalsEditor,
        goalsConverter
        ));
...
[Flags]
private enum OrcGoals
{
    WorldDomination = 1,
    Work = 2,
    Eat = 4,
    Sleep = 8,
}

This version of AttributePropertyDescriptor uses a value converter as well; both the value editor and converter use the enumeration OrcGoals, which specifies the range of orc goals. This value editor allows displaying multiple goals at the same time, as seen in the property editor:

These attributes are in the "Character attributes" category. Both the values "Eat" and "Sleep" are used for "Goals". The values of this attribute can be changed by selecting the attribute and then clicking the now visible button to display the editor.

The "orcType" attributes "level" and "emotion" both use a LongEnumEditor value editor, which only shows one value at a time. The value editor for each of these attributes is displaying both an image and a name for the current value. The value of an attribute can be changed by clicking the button on the right to display the list and then selecting an item.

The attributes "skill" and "weight" are displayed in the BoundedIntEditor value editor, which uses a slider to change the value.

EmbeddedCollectionEditor Class

An "orcType" object can contain other objects of these types:

  • "armorType"
  • "clubType"
  • "orcType"
This sample uses the EmbeddedCollectionEditor embedded property editor to add, remove, and edit items in these collections of armor, weapons, and children orcs.

After it is created, an EmbeddedCollectionEditor object must be configured by setting the following properties to methods to insert, remove, and move items in the collection:

/// <summary>
/// Gets or sets a delegate to retrieve the ItemInserters based on the current context</summary>
public Func<PropertyEditorControlContext, IEnumerable<ItemInserter>> GetItemInsertersFunc { get; set; }

/// <summary>
/// Gets or sets a delegate to remove selected child items</summary>
public Action<PropertyEditorControlContext, object> RemoveItemFunc { get; set; }

/// <summary>
/// Gets or sets a delegate to move selected child items</summary>
public Action<PropertyEditorControlContext, object, int> MoveItemFunc { get; set; }

For example, here's the delegate used for removing items:

var collectionEditor = new EmbeddedCollectionEditor();
...
collectionEditor.RemoveItemFunc = (context, item) =>
    {
        var list = context.GetValue() as IList<DomNode>;
        if (list != null)
            list.Remove(item.Cast<DomNode>());
    };

All these delegates first cast the collection, obtained from context.GetValue() as an IList<DomNode>. If the cast works, it removes the selected item from this list.

After setting up the EmbeddedCollectionEditor, the sample uses it as the value editor for the "orcType" attributes that represent collections:

string weaponCategory = "Weapons and Defense".Localize();
orcDescriptors.Add(
    new ChildPropertyDescriptor(
        "Armor".Localize(),
        Schema.orcType.armorChild,
        weaponCategory,
        "Armors".Localize(),
        false,
        collectionEditor
        ));

orcDescriptors.Add(
new ChildPropertyDescriptor(
        "Club".Localize(),
        Schema.orcType.clubChild,
        weaponCategory,
        "Club".Localize(),
        false,
        collectionEditor
        ));

orcDescriptors.Add(
new ChildPropertyDescriptor(
        "Orcs".Localize(),
        Schema.orcType.orcChild,
        "Children".Localize(),
        "Orc children".Localize(),
        false,
        collectionEditor
        ));

Note that the ChildPropertyDescriptor is used here, instead of AttributePropertyDescriptor:

public ChildPropertyDescriptor(
    string name,
    ChildInfo childInfo,
    string category,
    string description,
    bool isReadOnly,
    object editor)

The main difference between the descriptors is the addition of the ChildInfo parameter. The values used for this parameter are from the ChildInfo fields in the orcType subclass of Schema:

public static ChildInfo clubChild;
public static ChildInfo armorChild;
public static ChildInfo orcChild;

Here's what the "Weapons and Defense" category containing the "armorType" and "clubType" attributes looks like in the EmbeddedCollectionEditor, embedded in the property editor:

Note the buttons for "Armor": to add armor items, to remove selected armor, and or for moving selected armor items up or down. Clicking one of these buttons invokes the appropriate delegate specified previously for the EmbeddedCollectionEditor.

Similarly, the "Children" category contains an EmbeddedCollectionEditor for managing the collection of orc children. Each child is an "orcType" object, so it contains an EmbeddedCollectionEditor for its own "Weapons and Defense" and "Children" categories, and so on:

Finally, notice that the "orcType" object shown above contains a variety of value editors and value converters appropriate for the type of data, such as:

  • ColorPickerEditor and IntColorConverter for "skinColor".
  • FileUriEditor for "textureFile".
  • NumericMatrixEditor for "textureTransform", a matrix of 9 float values.
  • ArrayEditor for "textureArray".
  • FolderUriEditor for "resourceFolder".

Topics in this section

Clone this wiki locally