Published On: November 15, 2022Categories: Technical

The exchange of knowledge, skills and experiences of employees is one of our main priorities. In this blog post, we share another fix to a challenge of one of our colleagues encountered in his everyday tasks.

Very often we have a business requirement that when a user opens a dialog for the first time and enters values for parameters in it, he would like those values to be saves, such that the next time he opens the dialog he won’t need to input them again. This pattern is widely used in all versions of Dynamics and is based on SysLastValue table.

In RunBase classes, it is done through pack() and unpack() methods. The RunBase internal class state that needs to be preserved between runs is typically done through a local macro, together with a corresponding “version” of the stored data. In short terms, we save the packed state of the class with the corresponding version into the SysLastValue table record for this class and when we need to retrieve/unpack these values, we retrieve the version as we know it’s the first position in the container.

If the version is still the same as the current version, read the packed container into the variables specified in the local macro. If the version is different from the current version, return false, which will subsequently run initParmDefault() method to load the default values for the class state variables.

Problem description

It is not possible to extend macros, either global or locally defined. Macros are replaced with the corresponding text at compile time which would mean that all the existing code using the macros would need to be recompiled if you extended it, which is not an option.

Possible solution would be to use post-method handlers for pack/unpack methods and add additional state variables there, to the end of the container. This solution becomes very unreliable if there are more than two ISV solutions deployed on the environment.

Let’s consider an example where two ISV’s are deployed on an environment and post-method handler is implemented for handling state extension of a RunBase class.

Packing the class state

  1. The pack() method is run and returns a container with similar values as following:
[1, 11/2/2022, “VENDOR001”, 0].

  1. Post-method handler is called from the first ISV extension and returns the original container with additional specific state for the first ISV. Let’s assume that this is string field in the dialog with value “ISV1 Value”. The container will look like this:
[1, 11/2/2022, “VENDOR001”, 0, “ISV1 Value”].

  1. Next, post-method handler is called from the second ISV extension and the same logic will be applied here as well, so now the container will look like this:
  2. [1, 11/2/2022, “VENDOR001”, 0, “ISV1 Value”, “ISV2 Value”].

Unpacking the class state

  1. The best-case scenario would be to follow the same steps for unpacking execution. First, the unpack() method is executed and base class variables will get their values from the packed state.
  2. The unpack post-method handler is called for ISV1 extension and it needs to retrieve only the part of the container that is relevant to the ISV1 solution, that is “ISV1 Value”.
  3. Similar to the previous step, the post-method handler for ISV2 should unpack the “ISV2 Value” and assign it to the corresponding variable.

The unpacking scenario is highly risking and cannot be executed in reliable way, since the system cannot guarantee that post-method handlers will be executed in the same order for unpack as they were for pack.

Solution

In order to solve the problem described above, Microsoft has came up with a deterministic framework where each specific extended packed state can be uniquely identified by name, which allows ISV’s to call set & get for the state by it’s name.

 To achieve this, you need to use the SysPackExtensions class. Most important methods of the framework are:

  • packExtension() – adds the extended variable(s) to the end of the packed state container, prefixing it with the name of the extension.
  • unpackExtension()– looks through the packed state container and finds the sub-part for this particular extension based on extension name.
  • isCandidateExtension()– evaluates if the passed container is an extension packed state. For that it needs to consist of the name of the extension and packed state in a container.

To avoid collisions with other eventual extensions, prefix members and methods with some custom text. In the example, the prefix “axm” is used. This practice is important, because it helps prevent name clashes with other extensions and future versions of the augmented class.

In the following example, a toggle button control is added to the dialog box, the value of the control is get, in the run method action is performed on the value, and the value is serialized via the pack and unpack methods. The following example shows how to implement this scenario.

 

[ExtensionOf(classStr(SysUserLogCleanup))]final class AXMMySysUserLogCleanup_Extension
{
// static members
static private SysUserLogCleanup axmRunningInstance;

// Extending class state...
private boolean axmArchive;
private DialogField axmDialogArchive;
#define.CurrentVersion(1)
#localmacro.CurrentList
axmArchive
#endmacro

public Object dialog()
{
Dialog dialog = next dialog();

axmDialogArchive = dialog.addField(extendedtypestr(NoYesId), "Archive");
axmDialogArchive.value(axmArchive);

return dialog;
}

public boolean getFromDialog()
{
boolean result = next getFromDialog();
axmArchive = axmDialogArchive.value();
return result;
}

public void initParmDefault()
{
next initParmDefault();
axmArchive = true;
}

public void run()
{
try
{
axmRunningInstance = this;
next run();
}
finally
{
axmRunningInstance = null;
}
}

public container pack()
{
container packedClass = next pack();
return SysPackExtensions::appendExtension(packedClass, classStr(AXMMySysUserLogCleanup_Extension), this.axmPack());
}

private boolean axmUnpack(container packedClass)
{
Integer version = RunBase::getVersion(packedClass);
switch (version)
{
case #CurrentVersion:
[version, #currentList] = packedClass;
break;
default:
return false;
}
return true;
}

private container axmPack()
{
return [#CurrentVersion, #CurrentList];
}

public boolean unpack(container _packedClass)
{
boolean result = next unpack(_packedClass);

if (result)
{
container axmState = SysPackExtensions::findExtension(_packedClass, classStr(AXMSysUserLogCleanup_Extension));
//Also unpack the extension
if (!this.axmUnpack(axmState))
{
result = false;
}
}

return result;
}

private void axmArchiveUserLog(SysUserLog _userLog)
{
if (axmArchive)
{
//...
}
}

// Wire up event handler for deletion of the record
[DataEventHandler(tableStr(SysUserLog), DataEventType::Deleting)]static public void SysUserLog_onDeleting(Common _sender, DataEventArgs _e)
{
if (axmRunningInstance)
{
axmRunningInstance.axmArchiveUserLog(_sender as SysUserLog);
}
}
}