Automatic Class Pack/Unpack Part 2

In a previous post I demonstrated a way to pack & unpack a class in a manner that would always be version independent without the use of macros. In AX 2012 Microsoft added attribute support for classes and methods. When I discovered this new functionality, I knew it would be ideal for improving my old code. So I created a class named PackValueAttribute which extends the new SysAttribute class. This allows the programmer to simply tag a method with the new attribute and it’s value will automatically be included in any pack/unpack method calls.

There are a few rules that must be adhered to in the construction of the methods to ensure the automatic packing is successful:

  1. The method must be public
  2. The method must be an instance method (not static)
  3. If the return type is not a primitive, it must be packable
  4. The method’s first parameter must match the method’s return type

First you need to create the PackValueAttribute class which extends the new SysAttribute class.

class PackValueAttribute extends SysAttribute
{
}

Using the new attribute, this static method is used to pack the class into a container object:

static container PackClass(object _class)
{
    SysDictClass    tsdc, sdc = new SysDictClass(classIdGet(_class));
    Map             vars = new Map(Types::String,Types::Container);
    SysDictMethod   dictMethod;
    container       c;
    str             mName;
    Types           mType;
    SetEnumerator   senum;

    try
    {
        senum = sdc.methods(true,false,true).getEnumerator();
        while(senum.moveNext())
        {
            dictMethod = senum.current();
            //Method must have PackValueAttribute at 
            //least one parameter & a return object
            if(!(dictMethod.getAttribute(classStr(PackValueAttribute))
                    && dictMethod.parameterCnt()
                    && dictMethod.returnType() != Types::void))
                continue;

            //First parameter type must match return type
            if(dictMethod.returnId() != dictMethod.parameterId(1))
                continue;

            mName   = dictMethod.name();
            mType   = dictMethod.returnType();

            c = conNull();
            switch(mType)
            {
                //get container for parameters that return packable objects
                case Types::Class:
                    tsdc = new SysDictClass(dictMethod.returnId());
                    if(tsdc.hasStaticMethod('pack'))
                        c = sdc.callObject(mName,_class).pack();
                    break;
                //All others fit in container
                default:
                    c = [sdc.callObject(mName,_class)];
            }
            vars.insert(mName,c);
        }
    }
    catch (exception::Error)
    {
        return connull();
    }
    return vars.pack();
}

Using the new attribute, this static method is used to unpack the container object into a class:

static boolean UnpackClass(object _class, container _values)
{
    SysDictClass    tsdc, sdc = new SysDictClass(classIdGet(_class));
    SysDictMethod   dictMethod;
    SetEnumerator   senum;
    Object          o;
    container       c;
    Map             vars;
    str             mName;
    Types           mType;

    try
    {
        vars    = Map::create(_values);
        senum   = sdc.methods(true,false,true).getEnumerator();
        while(senum.moveNext())
        {
            dictMethod = senum.current();
            if(!dictMethod.getAttribute(classStr(PackValueAttribute)))
                continue;

            mName   = dictMethod.name();
            mType   = dictMethod.returnType();
            if(vars.exists(mName))
            {
                o       = null;
                c       = vars.lookup(mName);
                switch(mType)
                {
                    case Types::Class:
                        tsdc = new SysDictClass(dictMethod.returnId());
                        if(tsdc.hasStaticMethod('create'))
                            o = tsdc.callStatic('create',c);
                        else if(tsdc.hasObjectMethod('unpack'))
                            o = tsdc.callObject('unpack',tsdc.makeObject(),c);

                        if(o)
                            sdc.callObject(mName,_class,o);
                        break;

                    default:
                        sdc.callObject(mName,_class,conpeek(c,1));
                        break;
                }
            }
        }
    }
    catch
    {
        return false;
    }
    return true;
}

For the following example I created a class called AutoPackTools that contains the static pack/unpack methods above. I also created a demo class named SLC_PackTest. It contains two class variables with parameter styled methods that will be tagged as packable values.

The class declaration with the class variables.

class SLC_PackTest
{
    int     testInt;
    NoYes   testEnum;
}

The following methods are used as the public parameter methods that we want to include when we pack our class.

[PackValueAttribute]
public NoYes testEnum(NoYes _value = testEnum)
{
    testEnum = _value;
    return _value;
}

[PackValueAttribute]
public int testInt(int _value = testInt)
{
    testInt = _value;
    return _value;
}

Notice the line that precedes the method header [PackValueAttribute]. This tag allows us to mark a method as being one that we want included when we pack & unpack this class. Since we are using a version independent method for packing our class, we simply add new methods as needed and prefix them with this tag. The new values are automatically included the next time that the class is packed and in subsequent unpacks.

Here we can see the new format of the standard pack & unpack methods.

public container pack()
{
    return [1,AutoPackTools::PackClass(this)];
}

public boolean unpack(container _values)
{
    int version;
    container c;
    
    if(_values == connull()){ return false;}

    [version,c] = _values;
    return AutoPackTools::UnpackClass(this,c);
}

I have included a version marker strictly to maintain compatibility with the old Dynamics AX methodology of packing & unpacking. Using this new code it is completely unnecessary and there is no need to ever change the version being inserted into the container.

Therefore we could just as easily use the following more simplistic versions of these methods.

public container pack()
{
    return AutoPackTools::PackClass(this);
}

public boolean unpack(container _values)
{
    return AutoPackTools::UnpackClass(this,_values);
}

NOTE: Using this version of the methods would require that the container being passed into the UnpackClass method be in the proper format as created by the new PackClass method.

As you can see, the new attribute functionality in AX 2012 is extremely useful. You can even pass parameters to the tags that can be retrieved when inspecting the attributes of a class or method. More information on that can be found on the MSDN site by searching for SysAttribute.

UPDATE:

In response to Joe’s question I decided to add another class to the project. I created a new class named SLC_PackTest_Child that extends the class in the previous example. This is the class declaration with an additional class variable.

class SLC_PackTest_Child extends SLC_PackTest
{
    str     testStr;
}

The following method is the public parameter methods that we want to include when we pack our new child class.

[PackValueAttribute]
public str testStr(str _value = testStr)
{
    testStr = _value;
    return _value;
}

Since the child class inherits the pack & unpack methods from its parent, we do not need to override them here. Therefore we only need to add the create & main methods to this class to allow us to demonstrate the pack/unpack functionality.

//This method allows us to instantiate a new instance 
//of the class directly from it's pack method
public static SLC_PackTest create(container _packedObject)
{
    SLC_PackTest_Child    tst = new SLC_PackTest_Child();

    tst.unpack(_packedObject);
    return tst;
}

public static void main(Args args)
{
    SLC_PackTest_Child  tst1, tst2;
    SLC_PackTest        parent;

    tst1 = new SLC_PackTest_Child();
    tst1.testInt(35);
    tst1.testEnum(NoYes::Yes);
    tst1.testStr("Test");

    //Notice that the child class doesn't override pack
    //yet we can still call it from the child class
    tst2 = SLC_PackTest_Child::create(tst1.pack());
    //At this point tst1 is identical to tst2

    //The following lines are used to display the flexibility
    //of this new pack/upack methodology. Any packed class can 
    //be unpacked into any of its descendent or parent classes
    parent = SLC_PackTest::create(tst1.pack());
    //Only the properties that are common are populated
    tst1 = SLC_PackTest_Child::create(parent.pack());
}

This clearly demonstrates the simplicity of code maintenance associated with this new programming pattern. Any class that extends a packable class will automatically inherit the ability to have it’s packable attributes packed right along with those of it’s parent. The only coding required by the programmer is to add the [PackValueAttribute] to the variable’s encapsulation method.

The following file contains a project with the classes used in this post. AX2012_AutoPackUnpack.xpo

Share/Bookmark
This entry was posted in AX 2012, Microsoft Dynamics AX and tagged , , , , , . Bookmark the permalink.

2 Responses to Automatic Class Pack/Unpack Part 2

  1. Joe says:

    I have not tried your code, just glanced over it now, but how does your solution handle an extended class. i.e. super(). in this case i have a dialog that captures some basic information that is very common. each class that extends it(controlled constructor pattern, i.e. see sales form letter) add a new dialog field in the child class that has value that needs packing. so calling super in the pack and unpack brings both set of stored data together for the dialog. how would this work under you current concept?

    • steve says:

      Hi Joe,
      In this line of code in the pack/unpack methods, the third parameter indicates that inherited methods should be included in the returned set.
      senum = sdc.methods(true,false,true).getEnumerator();

      (If you look at the MSDN article on the SysDictClass.methods method it gives you the parameter information)

      That being the case, simply adding the tag to the methods of the super class will include them when the pack is called from a child class. Also note that if the pack method is implemented in the parent class, it does not need to be overridden in the child class when you use this pattern. The child class inherits the pack class from the parent.

      I have updated the attached XPO with an example of this functionality.

Comments are closed.