Pack Unpack in x++ :
Pack and unpack is
Dynamics language for serialization -- putting object-specific data in a
writeable format and later reading it back to create an object in the exact
same state.
Most of the
interactive objects in Dynamics will automatically check for saved user data
when invoked and instantiate accordingly. Perhaps you remember running a
report and the next time you opened the report your previous query and
parameter values were already there. This happens because of pack and
unpack.
The majority of what
makes pack and unpack confusing is macros, so let's do a simple example not
using macros at all. Let's say we have a class that has two date values I
need to persist between runs: startDate and endDate.
So when pack()
gets called, I need to store both variables. (If you don't understand the
way containers work, look them up on MSDN first, otherwise this won't make
sense).
container pack()
{
return [startDate, endDate];
}
{
return [startDate, endDate];
}
The framework will
take whatever pack returns and store it in user data. So when the object
is instantiated, unpack() gets called in order to reinitialize the values.
boolean
unpack(container _packedValues)
{
[startDate, endDate] = _packedValues;
return true;
}
{
[startDate, endDate] = _packedValues;
return true;
}
Follow what's
happening here? We expect _packedValues to contain startDate and endDate
in that order, so we assign those values to our class variables.
Now let's imagine
we want to add another class variable that needs to be stored: isPosted
To do this, we'll
have to modify our pack to return
[startDate,
endDate, isPosted]
and our unpack to
assign all three:
[startDate,
endDate, isPosted] = _packedValues;
Hm, but what about
values that were stored before our modification? That container
assignment won't work because _packedValues will only contain 2 values
(startDate and endDate). So let's make the first value in our packed data
identify the version of the packed data so we can handle old values without
crashing.
container pack()
{
return [1, startDate, endDate, isPosted];
}
{
return [1, startDate, endDate, isPosted];
}
boolean
unpack(container _packedValues)
{
{
boolean ret = true;
int version;
int version;
;
if (conpeek(_packedValues) == 1)
{
[version, startDate, endDate, isPosted] = _packedValues;
}
else
{
ret = false;
}
return ret;
}
if (conpeek(_packedValues) == 1)
{
[version, startDate, endDate, isPosted] = _packedValues;
}
else
{
ret = false;
}
return ret;
}
Ok, so now if I
need to add more variables, I need to change the version number in both pack
and unpack, but now old data will make unpack() return false, which will
initialize the parameter defaults.
So now let's add
some macros so we only have to change one spot when we change what we want to
pack. In our classDeclaration, we'll create 2 macros: one for the version
of the data, and one for the values we wish to store.
classDeclaration()
{
#define.CurrentVersion(1)
#localMacro.CurrentList
startDate,
endDate,
isPosted
#endMacro
}
{
#define.CurrentVersion(1)
#localMacro.CurrentList
startDate,
endDate,
isPosted
#endMacro
}
If you're
unfamiliar with macros, what they do is fill in exactly what the macro says at
compile time. So whenever you write #CurrentVersion, the compiler fills
in '1'. Whenever you write #CurrentList, the compiler fills in
'startDate, endDate, isPosted'. So now we can change our pack() and
unpack() to look like this:
container pack()
{
return [#CurrentVersion, #CurrentList];
}
{
return [#CurrentVersion, #CurrentList];
}
boolean
unpack(container _packedValues)
{
int version;
boolean ret = true;
;
if (conpeek(_packedValues) == #CurrentVersion)
{
[version, #CurrentList] = _packedValues;
}
else
{
ret = false;
}
{
int version;
boolean ret = true;
;
if (conpeek(_packedValues) == #CurrentVersion)
{
[version, #CurrentList] = _packedValues;
}
else
{
ret = false;
}
return ret;
}
}
This is pretty close to what you see in most pack() and unpack()
methods. I hope this helped clear things up.
They are typically used in classes
and reports, but you can use them in forms too (reports and forms are
technically classes). (I don't see any reason why to use them in queries.)
It all depends on what you want to
do and whether all references can be serialized.
A trivial implementation could look
like this:
class SerializableObject
{
// object state - all data you need to serialize
int state1;
bool state2;
container pack()
{
// state is serialized to container (=
binary data)
return [state1, state2];
}
boolean unpack(container _packedClass)
{
// state is deserialized from
container back to instance variables
[state, state2] = _packedClass;
return true;
}
}
Nevertheless such an implementation
is too simple - it's difficult and error-prone to add additonal state
fields. That's why the usual pattern uses #CurrentList, #CurrentVersion
and such things.
So when pack()
gets called, I need to store both variables. (If you don't understand the
way containers work, look them up on MSDN first, otherwise this won't make
sense).
container pack()
{
return [startDate, endDate];
}
{
return [startDate, endDate];
}
The framework will
take whatever pack returns and store it in user data. So when the object
is instantiated, unpack() gets called in order to reinitialize the values.
boolean
unpack(container _packedValues)
{
[startDate, endDate] = _packedValues;
return true;
}
{
[startDate, endDate] = _packedValues;
return true;
}
Follow what's
happening here? We expect _packedValues to contain startDate and endDate
in that order, so we assign those values to our class variables.
Now let's imagine
we want to add another class variable that needs to be stored: isPosted
To do this, we'll
have to modify our pack to return
[startDate,
endDate, isPosted]
and our unpack to
assign all three:
[startDate,
endDate, isPosted] = _packedValues;
Hm, but what about
values that were stored before our modification? That container
assignment won't work because _packedValues will only contain 2 values
(startDate and endDate). So let's make the first value in our packed data
identify the version of the packed data so we can handle old values without
crashing.
container pack()
{
return [1, startDate, endDate, isPosted];
}
{
return [1, startDate, endDate, isPosted];
}
boolean
unpack(container _packedValues)
{
{
boolean ret = true;
int version;
int version;
;
if (conpeek(_packedValues) == 1)
{
[version, startDate, endDate, isPosted] = _packedValues;
}
else
{
ret = false;
}
return ret;
}
if (conpeek(_packedValues) == 1)
{
[version, startDate, endDate, isPosted] = _packedValues;
}
else
{
ret = false;
}
return ret;
}
Ok, so now if I
need to add more variables, I need to change the version number in both pack
and unpack, but now old data will make unpack() return false, which will
initialize the parameter defaults.
So now let's add
some macros so we only have to change one spot when we change what we want to
pack. In our classDeclaration, we'll create 2 macros: one for the version
of the data, and one for the values we wish to store.
classDeclaration()
{
#define.CurrentVersion(1)
#localMacro.CurrentList
startDate,
endDate,
isPosted
#endMacro
}
{
#define.CurrentVersion(1)
#localMacro.CurrentList
startDate,
endDate,
isPosted
#endMacro
}
If you're
unfamiliar with macros, what they do is fill in exactly what the macro says at
compile time. So whenever you write #CurrentVersion, the compiler fills
in '1'. Whenever you write #CurrentList, the compiler fills in
'startDate, endDate, isPosted'. So now we can change our pack() and
unpack() to look like this:
container pack()
{
return [#CurrentVersion, #CurrentList];
}
{
return [#CurrentVersion, #CurrentList];
}
boolean
unpack(container _packedValues)
{
int version;
boolean ret = true;
;
if (conpeek(_packedValues) == #CurrentVersion)
{
[version, #CurrentList] = _packedValues;
}
else
{
ret = false;
}
{
int version;
boolean ret = true;
;
if (conpeek(_packedValues) == #CurrentVersion)
{
[version, #CurrentList] = _packedValues;
}
else
{
ret = false;
}
return ret;
}
}
This is pretty
close to what you see in most pack() and unpack() methods. I hope this
helped clear things up.
0 comments:
Post a Comment