Meta Data

The meta data classes of the C++ API are ported to the .NET API just like all other Maya API classes. In addition, several enhancements have been made to take advantage of the power of .NET.

Metadata is a very important part of the Maya API, and enables clients to describe their custom data (its structure, the types, how to access the data per element, and so forth). .NET features can make the process much simpler and more powerful than it is in C++.

.NET Examples

Several interesting examples that demonstrate the .NET enhancements described on this page are included with the examples that are shipped with Maya (see Compiling examples). Open the examples.sln solution (in the devkit\dotnet\examples folder of your Developer Kit installation; see Setting up your build environment: Windows environment (64-bit)) using Microsoft Visual Studio, and you can find these examples in the MetaData folder. The following are some interesting examples:

Summary

The following is a list of the main classes of the metadata API, and a quick summary of each along with a list of C++ methods that have been ported to .NET:

Associations

This is an entry point to access the meta data from a Maya object (for example, by getting the MFnDependencyNode.metadata property).

It’s basically a simple container of channels.

A few interesting methods ported from C++ are:

  • Channel findChannel(string) searches for a channel given its name
  • void setChannel(Channel) adds a channel, or replaces an existing channel that has the same name
  • bool removeChannel(string) removes a channel given its name
  • AssociationsIterator begin() returns a C++/STL-style iterator to iterate over the channels

Channel

This is a collection of streams that are grouped together, either because they share the same topology (the same stream indices) or because the user decides they should logically be in the same group (for example, all the streams for a simulation effect would be in the same channel and given a channel name that is the name of the effect).

A channel can be named, but some names are reserved (for example, vertex is reserved to indicate that its streams have one element per vertex).

A few interesting methods ported from C++ are:

  • Stream findDataStream(string) looks for a stream given its name
  • Stream setDataStream(Stream) adds a stream
  • void removeDataStream(string) removes a stream given its name
  • ChannelIterator begin() returns a C++/STL-style iterator to iterate over the streams
  • bool removeElement(Index elementIndex) removes the specified element from every stream found in the channel
  • bool addElement(Index elementIndex) adds the specified element to every stream found in the channel

Stream

This is a container of data on the object (for example, on a mesh, in the channel vertex, you have as many data elements as you have vertices in the stream).

A few interesting methods ported from C++ are:

  • Stream(Structure, string) creates a Stream object using the specified structure and given the specified name
  • bool setElement(Index, Handle) replaces the data at a given index with that of another
  • Handle element(Index) obtains a handle of the data at a given index
  • StreamIterator begin() returns a C++/STL-style iterator to iterate over the handles

Structure

This class describes the meta data found at each element of the stream.

Its method AddMember is used to define each field of the meta data.

For instance, if you want one float and one boolean to be stored at every element, first create a structure and call AddMember(kFloat,1,”MyFloatField”) and AddMember(kBoolean,1,”MyBooleanField”).

A few interesting methods ported from C++ are:

  • bool addMember( Member.eDataType, uint arraySize, string memberName ) adds a member with the specified type, size (typically 1), and name to the structure
  • StructureIterator begin() returns a C++/STL-style iterator to iterate over the members

Handle

This class allows you to access the data for a specified element of the stream.

With a handle, you can get or set the data of the stream element.

A few interesting methods ported from C++ are:

  • bool setPositionByMemberIndex(uint) indicates the member of the structure that this handle is associated with
  • int[] asInt32Array gets or sets the integer data as an array of integers (even if there is only one integer)
  • float[] asFloatArray gets or sets the float data as an array of floats (even if there is only one float)
  • ... and so forth

Taking advantage of .NET

.NET provides several enhancements to the metadata API. Here are the main classes of the API with the .NET enhancements:

Associations

Because it acts as a dictionary of Channel objects, the class supports the .NET interface IDictionary<Channel>.

This interface enables you to use the following standard methods/properties that are expected from a dictionary in .NET:

  • Channel channel;
    if( myAssociations.TryGetValue( “vertex”, out channel ) { ... }

    This method allows you to look for a channel given its name and tests for its existence at the same time.

  • Channel channel =  myAssociations[“vertex”];

    This indexer also looks for a channel given its name, but in this case, the channel is expected to be found (throws KeyNotFoundException otherwise).

  • if(myAssociations.ContainsKey( “vertex” ) { ... }

    This simply tests if a channel with the specified name exists. The channel is not returned.

  • myAssociations.Add( “vertex”, myChannel );

    This adds the channel, or throws ArgumentException if a channel with the same name already exists.

  • foreach( Channel chnl in myAssociations )  { ... }

    This lets you enumerate all the channels using this very simple syntax instead of the complicated C++/STL syntax:

    for( AssociationsIterator iter = myAssociations.begin(); iter.notEqual( myAssociations.end() ); iter.next() ) { 
        Channel chnl = iter.__deref__();
        ... 
    }

Channel

Because it acts as a dictionary of Stream objects, the class supports the .NET interface IDictionary<Stream>.

This interface enables you to use the following standard methods/properties that are expected from a dictionary in .NET:

  • Stream stream; 
    if( myChannel.TryGetValue( “myStream”, out stream ) { ... }
    This method allows you to look for a stream given its name and tests for its existence at the same time.
  • Stream stream =  myChannel[“myStream”]; 
    This indexer also looks for a stream given its name, but in this case, the stream is expected to be found (throws KeyNotFoundException otherwise).
  • if( myChannel.ContainsKey( “myStream” ) { ... }
    This simply tests if a stream with the specified name exists. The stream is not returned.
  • myChannel.Add( “myStream”, myStream );
    This adds the stream or throws ArgumentException if a stream with the same name already exists.
  • foreach( Stream stream in myChannel )  { ... }
    This lets you enumerate all the streams using this very simple syntax instead of the complicated C++/STL syntax:
    for( ChannelIterator iter = myChannel.begin(); iter.notEqual( myChannel.end() ); iter.next() ) { 
        Stream stream = iter.__deref__();
        ... 
    }

Stream

Because it acts as a dictionary of Handle objects, the class supports the .NET interface IDictionary<Handle>.

This enables you to use the following standard methods/properties that are expected from a dictionary in .NET:

  • Handle handle; 
    if( myStream.TryGetValue( 18, out handle ) { ... }
    This method allows you to look for a handle given its index and tests for its existence at the same time.
  • Handle handle = myStream[18]; 
    This indexer also looks for a handle given its index, but in this case, the handle is expected to be found (throws KeyNotFoundException otherwise).
  • if( myStream.ContainsKey(18) { ... }
    This simply tests if a handle with the specified index exists. The handle is not returned.
  • myStream.Add( 18, myHandle );
    This adds the handle or throws ArgumentException if a handle with the same index already exists.
  • foreach( Handle handle in myStream )  { ... }
    This lets you enumerate all the handles using this very simple syntax instead of the complicated C++/STL syntax:
    for( StreamIterator iter = myStream.begin(); iter.notEqual( myStream.end() ); iter.next() ) { 
        Handle handle = iter.__deref__();
        ... 
    }

Structure

IEnumerable<Member> was added in order to enable this syntax:

foreach( Member member in myStructure )  { ... }

This lets you enumerate all the members using this very simple syntax instead of the complicated C++/STL syntax:

for( StructureIterator iter = myStream.begin(); iter.notEqual( myStream.end() ); iter.next() ) { 
    Member member = iter.__deref__();
    ... 
}

Handle

The C++ API uses a single method using pointers (for example, int *asInt32()) to let you access the data for a given type. You can use it as a pointer to a single integer (for example, myInteger = *asInt32()) or an array of integer values (for example, asInt32()[6]) if you had specified a size of greater than 1 when you created the corresponding member in the Structure object.

For .NET, using pointers in the API was not feasible so two properties are provided per type:

  • int[] asInt32Array to get or set the integer data as an array of integers
  • int asInt32 to get or set the single integer (will throw an ApplicationException if the size found in the corresponding structure member is greater than 1)
  • float[] asFloatArray to get or set the float data as an array of floats
  • float asFloat to get or set the single float (will throw an ApplicationException if the size found in the corresponding structure member is greater than 1)
  • ... and so forth

Matching a client .NET type: the ultimate technique to simplify data description and access

Using the Structure class to describe the data of an element and then the Handle class to access the data is quite low-level and C++ like, for example:

.NET simplifies the whole process, as long as you have a class or structure that describes the data where each member of your type is a member in the expected Structure object.

The new StructureClass assembly attribute

In your plug-in, if you use the new StructureClass assembly attribute and provide your type that describes the structure of your metadata, .NET can automatically create the Structure object for you:

[assembly: StructureClass(typeof(MyNameSpace.MyStructureClass))]

namespace MyNameSpace
{
   public class MyStructureClass
   {
      public int a;
      public int b { get; set; }
      public float[] c = new float[3];
   }
}

Thanks to introspection, .NET can find all the public fields and properties in your class (or struct) and automatically create a Structure object that is registered using Structure.registerStructure.

The name of the Structure is the full name of your type (for example, MyNameSpace.MyStructureClass). The name of each Structure member is the name of the corresponding member in your type.

Converting from a Handle to a .NET object or vice-versa

If you use the new StructureClass assembly attribute (see previous section) with the type of a .NET object, you can use the new following methods of the Handle class:

  • public void SetFromDotNetObject( Object dotNetObj, Structure structure );

    This does a member wise copy from your .NET object to the Handle object.

    This is the equivalent of the following pseudo-code:

    for each public member in the type of dotNetObj 
        find corresponding member in Structure object
        call this.setPositionByMemberIndex( index of member found ) 
        if type is int32
            this.asInt = get value of member in dotNetObj
        else if type is float
            this.asFloat = get value of member in dotNetObj
        …
    next
  • public void CopyToDotNetObject(Object dotNetObj, Structure structure);

    This does a member wise copy from the Handle object to your .NET object.

    This is the equivalent of the following pseudo-code:

    for each public member in the type of dotNetObj 
        find corresponding member in Structure object
        call this.setPositionByMemberIndex( index of member found ) 
        if type is int32
            dotNetObj.SetValue( this.asInt )
        else if type is float
            dotNetObj.SetValue( this.asFloat )
        …
    next
    

These methods become unnecessary, however, when you use the new generic class StreamForType<T> explained in the next section.

StreamForType<T>: special version of the Stream class tailored to your .NET type

While Stream is the class to use when you can’t provide a .NET type that describes your Structure, the class StreamForType<T> should be used when you have provided your type with the new StructureClass assembly attribute.

Use the same type for the T parameter of this generic class and you are given a Stream-like class that is completely tailored for your type.

This generic class is a wrapper to a Stream object so you can create it in two ways. The following are the two constructors:

  • StreamForType<T>(string streamName)

    This constructor looks for an existing Structure called with the full name of your type T (it should find it if you used the StructureClass assembly attribute). It then creates a new Stream object using the specified name and the structure found and wraps this new Stream object.

  • StreamForType<T>(Stream baseStream)

    This constructor wraps the specified stream but also ensures that the structure of this base stream is compatible with your type (it should be if you used the StructureClass assembly attribute).

While the Stream class can be seen as a dictionary of Handle objects, the StreamForType<T> generic class can be seen as a dictionary of T objects where T is your .NET class (or struct). This is done with the IDictionary<T> supported by the generic class.

The following are a few interesting methods:

  • bool TryGetValue(Index, out T value)

    Finds the Handle object with the specified index, then calls handle.CopyToDotNetObject() on a new instance of T. It returns the new instance; or, if the handle is not found, it returns false.

  • void Add(Index, T value)

    Creates a new handle at the specified index, then calls handle.SetFromDotNetObject() using the specified T value.

  • T this[Index key]

    This enables the following syntax (MyStructureClass is the .NET type used for the T parameter):

    MyStructureClass myDotNetObj ;
    myDotNetObj.myInt = 3;
    myStream[18] = myDotNetObj;

    The example above adds the .NET object at the specified index or replaces the current object if one already exists at that index.

    MyStructureClass myDotNetObj = myStream[18];

    The example above returns the object found at the specified index or throws a KeyNotFoundException if an object does not exist at that index.

    Be careful though, the following syntax compiles but does not work as expected as it does not change the element in the base stream:

    myStream[18].myInt = 1.6;

    Instead, the example above changes the field myInt of the copy returned by the indexer, so it is completely useless.

    For the desired result, do as follows:

    MyStructureClass temp = myStream[18];
    temp.myInt = 1.6;
    myStream[18] = temp;
    

  • IEnumerator<T> GetEnumerator()

    This enumerates all your elements with this simple syntax:

    foreach( MyStructureClass myDotNetObj in myStream ) { … }

Using the power LINQ to perform SQL-like queries

Since IEnumerable is supported on most classes, you can perform very powerful queries with your data.

For instance, if your class has a field called amplitude, and you want to find the metadata in the vertex channel of a mesh where the amplitude is greater than 0.5, you can do this easily as follows:

 Channel chn = myAssociations["vertex"];
 Stream chnStream = chn[“myStream”];
 var strm = new StreamForType<MyStructureClass>(chnStream);
 var list = strm.Where( ( MyStructureClass obj ) =>obj.amplitude >0.5 );
 foreach( MyStructureClass obj in list ) { … }

To query the vertex index, you can do as follows:

var list = strm.Where( ( KeyValuePair<Index,MyStructureClass> keyvalue ) => keyvalue.Value.amplitude > 0.5 );
foreach (var keyvalue in list ) {  
   Index index = keyvalue.Key;
   MyStructureClass obj = keyvalue.Value;
   …   
}

This type of query can be done on any of the following classes: Associations, Channel, Stream and Structure because they all implement IEnumerable, which is the only interface LINQ requires.