Brewsterware

October 10, 2021

Sys Operation framework template classes

Filed under: 365 for Finance and Operations — Tags: — Joe Brewer @ 6:04 pm

I’ve always thought that the dynamics community was super generous with the helpful blog posts. For years I would go back to this post whenever I had to implement classes for a sys operation framework process, and this post to remind me how to use reread, refresh and research. There are many awesome blogs out there with solutions for difficult problems whether they are common or not. I honestly don’t know where my career would be without them.

I hope this collection of classes, which an be used as a starting point for any sys operation framework process, will be useful for the community.

The following contract class demonstrates how to group and order parameters, add a query and add validation.

[
    DataContract,
    SysOperationContractProcessing(classStr(SysOpExampleUIBuilder)),
    SysOperationGroupAttribute('Group1', "label for group 1", '1'),
    SysOperationGroupAttribute('Group2', "label for group 2", '2')
]
class SysOpExampleContract extends SysOperationDataContractBase implements SysOperationValidatable
{
    private NoYes yesOrNo;
    private CustAccount custAccount;
    private str packedQuery;

    [
        DataMember,
        SysOperationLabel(literalStr("@SYS4082047")),
        SysOperationDisplayOrderAttribute('1'),
        SysOperationGroupMemberAttribute('Group1')
    ]
    public NoYes parmYesOrNo(NoYes _yesOrNo = yesOrNo)
    {
        yesOrNo = _yesOrNo;
        return yesOrNo;
    }

    [
        DataMember,
        SysOperationDisplayOrderAttribute('1'),
        SysOperationGroupMemberAttribute('Group2')
    ]
    public CustAccount parmCustAccount(CustAccount _custAccount = custAccount)
    {
        custAccount = _custAccount;
        return custAccount;
    }

    // substitute the query below with another as needed. Add ranges as needed to the query
    // to expose the fields on the batch job parameters form
    [
        DataMember,
        AifQueryType('_packedQuery', queryStr(smmCustTable))
    ]
    public str parmPackedQuery(str _packedQuery = packedQuery)
    {
        packedQuery = _packedQuery;
        return packedQuery;
    }

    public Query getQuery()
    {
        return new Query(SysOperationHelper::base64Decode(packedQuery));
    }

    public void setQuery(Query _query)
    {
        packedQuery = SysOperationHelper::base64Encode(_query.pack());
    }

    public boolean validate()
    {
        boolean isValid;

        isValid = true;

        return isValid;
    }
}

The following controller class demonstrates how to modify contract parameters before the batch dialog loads, how to force a batch job to run in batch and not interactively and how to create a task with a time delay.

[SysOperationJournaledParametersAttribute(true)] // this attribute allows a user to create a batch task from the batch tasks form
class SysOpExampleController extends SysOperationServiceController
{
    private static ClassDescription description()
    {
        return "description of the class";
    }

    protected void new(IdentifierName _className = '', IdentifierName _methodName = '', SysOperationExecutionMode _executionMode = SysOperationExecutionMode::Asynchronous)
    {
        IdentifierName parmClassName = _className != '' ? _className : classStr(SysOpExampleService);
        IdentifierName parmMethodName = _methodName != '' ? _methodName : methodStr(SysOpExampleService, run);

        super(parmClassName, parmMethodName, _executionMode);

        this.parmDialogCaption(SysOpExampleController::description());
    }

    // adding the method below will set the batch processing slider to "Yes" and disable it so that the process cannot be run interactively
    public boolean mustGoBatch()
    {
        return true;
    }

    public static SysOpExampleController newFromArgs(Args _args)
    {
        SysOpExampleController controller;
        SysOpExampleContract contract;

        controller = new SysOpExampleController();
    
        controller.initializeFromArgs(_args);

        contract = controller.getDataContractObject('_contract');

        contract.parmYesOrNo(NoYes::Yes);

        return controller;
    }

    public static void main(Args _args)
    {
        SysOpExampleController controller;

        controller = SysOpExampleController::newFromArgs(_args);
        controller.startOperation();
    }

    // the method below will create a batch task for this controller that will execute in 30 seconds
    public static void startWithDelay(Args _args)
    {
        const int DelayInSeconds = 30;
        SysOpExampleController controller;
        BatchHeader batchHeader;
        utcDateTime delayedStartTime;
        utcDateTime systemDateTime;

        controller = SysOpExampleController::newFromArgs(_args);
        controller.parmShowDialog(false);
        controller.parmLoadFromSysLastValue(false);

        systemDateTime = DateTimeUtil::getSystemDateTime();
        delayedStartTime = DateTimeUtil::addSeconds(systemDateTime, DelayInSeconds);

        batchHeader = BatchHeader::construct();

        batchHeader.parmCaption("Add a unique caption here (based on values in _args) so that the batch job can be easily found in the batch jobs form");

        // note that the two lines below will force a notification to the user on the job status when it has finished
        batchHeader.parmStartDateTime(delayedStartTime);
        batchHeader.parmAlerts(NoYes::Yes, NoYes::Yes, NoYes::Yes, NoYes::Yes, NoYes::No);
        
        batchHeader.addTask(controller);

        batchHeader.save();
    }
}

The following service class demonstrates how to use the RunbaseProgress class to let the user know the task progress and exception handing (including transient sql errors)

class SysOpExampleService extends SysOperationServiceBase
{
    private static int defaultMaxRetryOfTransientSqlConnectionError = 5;

    public void run(SysOpExampleContract _contract)
    {
        #OCCRetryCount
        RunbaseProgress progress;
        CustTable custTable;
        Query query;
        QueryRun queryRun;
        int64 totalRecords;

        try
        {
            ttsbegin;

            query = _contract.getQuery();
            queryRun = new QueryRun(query);

            totalRecords = SysQuery::countTotal(queryRun);

            // this progress class will update the SysProgress table in a separate transaction which
            // will be shown on the "Batch tasks" form - very useful for long running processes in batch :)
            progress = this.getProgressController(_contract);
            progress.setTotal(totalRecords);

            while (queryRun.next())
            {
                custTable = queryRun.get(tableNum(CustTable));

                // perform some logic on CustTable here...

                progress.incCount();
            }

            ttscommit;
        }
        catch (Exception::Deadlock)
        {
            retry;
        }
        catch (Exception::UpdateConflict)
        {
            if (appl.ttsLevel() == 0)
            {
                if (xSession::currentRetryCount() >= #RetryNum)
                {
                    throw Exception::UpdateConflictNotRecovered;
                }
                else
                {
                    retry;
                }
            }
            else
            {
                throw Exception::UpdateConflict;
            }
        }
        catch (Exception::DuplicateKeyException)
        {
            if (appl.ttsLevel() == 0)
            {
                if (xSession::currentRetryCount() >= #RetryNum)
                {
                    throw Exception::DuplicateKeyExceptionNotRecovered;
                }
                else
                {
                    retry;
                }
            }
            else
            {
                throw Exception::DuplicateKeyException;
            }
        }
        catch (Exception::TransientSqlConnectionError)
        {
            if (SysOpExampleService::retryTransientSqlConnectionError(3))
            {
                retry;
            }
            else
            {
                throw;
            }
        }
        catch
        {
            // this line will catch any exceptions that are thrown and not caught by the catch blocks above
            // and will show the line of the code that threw the exception in the debugger if the debugger is attached
            // If the debugger is not attached, the stack trace will be written to the event log - very useful
            // for troubleshooting intermittent issues
            Debug::assert(false);
        }
        finally
        {
            // any cleanup logic should go here..
        }
    }

    // the methods below would be better suited in a separate helper class so that they can be used with other batch jobs
    static boolean retryTransientSqlConnectionError(int _maxRetryOfTransientSqlConnectionError)
    {
        var currentRetryCount = xSession::currentRetryCount();
        var shouldRetry = currentRetryCount < _maxRetryOfTransientSqlConnectionError;

        if (shouldRetry)
        {
            SysOpExampleService::delayRetry(currentRetryCount);
        }

        return shouldRetry;
    }

    static internal void delayRetry(int _retryCount)
    {
        var delay = 5000 * power(2, min(_retryCount, 5));
        sleep(min(60 * 1000, delay));
    }
}

The following User Interface Builder class demonstrates how to add custom lookup methods and modified events for methods in the contract class

class SysOpExampleUIBuilder extends SysOperationAutomaticUIBuilder
{
    private SysOpExampleContract contract;
    private DialogField dlgFldYesOrNo;
    private DialogField dlgFldCustAccount;

    public void postBuild()
    {
        super();

        contract = this.dataContractObject();

        dlgFldYesOrNo = this.bindInfo().getDialogField(contract, methodStr(SysOpExampleContract, parmYesOrNo));
        dlgFldCustAccount = this.bindInfo().getDialogField(contract, methodStr(SysOpExampleContract, parmCustAccount));

        dlgFldYesOrNo.registerOverrideMethod(
            methodStr(FormCheckBoxControl, modified), 
            methodStr(SysOpExampleUIBuilder, yesOrNoModified), 
            this);

        dlgFldCustAccount.registerOverrideMethod(
            methodStr(FormStringControl, lookup), 
            methodStr(SysOpExampleUIBuilder, lookupCustomer), 
            this);
    }

    public boolean yesOrNoModified(FormCheckBoxControl _control)
    {
        dlgFldCustAccount.enabled(_control.value());

        return true;
    }

    public void lookupCustomer(FormStringControl _control)
    {
        SysTableLookup sysTableLookup;

        sysTableLookup = SysTablelookup::newParameters(tableNum(CustTable), _control, true);
        sysTableLookup.addLookupfield(fieldNum(CustTable, AccountNum));
        sysTableLookup.addLookupMethod(tableMethodStr(CustTable, name));

        sysTableLookup.performFormLookup();
    }
}

September 30, 2020

Sudoku cracker for Dynamics 365 Finance and Operations

Filed under: 365 for Finance and Operations — Joe Brewer @ 12:44 pm

It’s been a long time since I have done any coding just for fun, and I rarely do it with X++, but a sudoku cracker for Finance and Operations seemed like a good challenge.

This cracker will crack even the most toughest of puzzles by using recursion and backtracking for when there are multiple possibilities for values in a cell.

/// <summary>
/// Sudoku puzzle cracker
/// </summary>
class SudokuCracker
{
    // this macro allows us to specify the line and column of the grid array
    #localMacro.GridIndex
        (%1 - 1) * 9 + %2
    #endMacro

    private const int TotalRows = 9;
    private const int TotalColumns = 9;

    private int grid[TotalRows, TotalColumns];
    private int cellsFilled;

    /// <summary>
    /// Runs the class with the specified arguments.
    /// </summary>
    /// <param name = "_args">The specified arguments.</param>
    public static void main(Args _args)
    {
        SudokuCracker cracker = new SudokuCracker();

        cracker.loadGrid();
        cracker.solve();
        cracker.saveGrid();
    }

    /// <summary>
    /// import a file with the known numbers and populate a 2 dimensional array with them
    /// </summary>
    public void loadGrid()
    {
        System.String stringLine;
        str line;
        int gridLine = 0;
        var fileUpload = File::GetFileFromUser() as FileUploadTemporaryStorageResult;

        using (var reader = new System.IO.StreamReader(fileUpload.openResult()))
        {
            stringLine = reader.ReadLine();

            while (!System.String::IsNullOrEmpty(stringLine))
            {
                line = strKeep(stringLine, '123456789 ');

                if (line)
                {
                    gridLine++;

                    for (int i = 1; i <= TotalRows; i++)
                    {
                        int gridIndex = #GridIndex(gridLine, i);
                        int value = str2Int(subStr(line, i, 1));

                        grid[gridIndex] = value;

                        if (value)
                        {
                            cellsFilled++;
                        }
                    }
                }

                stringLine = reader.ReadLine();
            }
        }
    }

    /// <summary>
    /// create a pretty grid with the numbers filled in, and send it back to the user
    /// </summary>
    public void saveGrid()
    {
        TextBuffer output;

        output = new TextBuffer();

        for (int line = 1; line <= TotalRows; line++)
        {
            output.appendText(strFmt('%1%2%3|%4%5%6|%7%8%9\r\n', 
                grid[#GridIndex(line, 1)], 
                grid[#GridIndex(line, 2)], 
                grid[#GridIndex(line, 3)],
                grid[#GridIndex(line, 4)],
                grid[#GridIndex(line, 5)],
                grid[#GridIndex(line, 6)],
                grid[#GridIndex(line, 7)],
                grid[#GridIndex(line, 8)],
                grid[#GridIndex(line, 9)]));

            if (line mod 3 == 0 &&
                line != TotalRows)
            {
                output.appendText('---+---+---\r\n');
            }
        }

        File::SendStringAsFileToUser(output.getText(), 'solved.txt');
    }

    /// <summary>
    /// method to determine whether a value is valid at a specific line and column
    /// </summary>
    /// <param name="_line">The line number of the grid</param>
    /// <param name="_column">The column number of the grid</param>
    /// <param name="_value">An integer value to be tested</param>
    /// <returns>true if the value is valid, false if not</returns>
    private boolean isValuePossible(int _line, int _column, int _value)
    {
        // check to see whether the value is possible in the line
        for (int i = 1; i <= TotalRows; i++)
        {
            if (grid[#GridIndex(_line, i)] == _value)
            {
                return false;
            }
        }

        // check to see whether the value is possible in the column
        for (int i = 1; i <= TotalColumns; i++)
        {
            if (grid[#GridIndex(i, _column)] == _value)
            {
                return false;
            }
        }

        // check to see whether the value is possible in the square

        // work out the starting point for the square
        int line = real2int(roundDownDec((_line - 1) / 3, 0)) * 3;
        int column = real2int(roundDownDec((_column - 1) / 3, 0)) * 3;

        for (int i = 1; i <= 3; i++)
        {
            for (int j = 1; j <= 3; j++)
            {
                int gridIndex = #GridIndex(line + i, column + j);

                if (grid[gridIndex] == _value)
                {
                    return false;
                }
            }
        }

        return true;
    }

    /// <summary>
    /// recursive backtracking method which fills in the missing numbers to solve the puzzle
    /// </summary>
    public void solve()
    {
        for (int line = 1; line <= TotalRows; line++)
        {
            for (int column = 1; column <= TotalColumns; column++)
            {
                if (grid[#GridIndex(line, column)] == 0)
                {
                    for (int value = 1; value <= 9; value++)
                    {
                        if (this.isValuePossible(line, column, value))
                        {
                            cellsFilled++;
                            grid[#GridIndex(line, column)] = value;

                            this.solve();

                            // have we finished?
                            if (cellsFilled == (TotalRows * TotalColumns))
                            {
                                return;
                            }

                            // start backtracking
                            cellsFilled--;
                            grid[#GridIndex(line, column)] = 0;
                        }
                    }

                    // nothing to see here, move along please.....
                    return;
                }
            }
        }
    }
}

The format of the file that this class uses is an ASCII based grid using pipes, hyphens and plus symbols to separate the nine squares of the sudoku grid. Below is an example that can be used to test the code – copy it into notepad and save it somewhere where the 365FO client can access it.

  9| 85|   
 6 |   |  9
 78|   | 14
---+---+---
   |   |   
  5| 18|   
   |7  |482
---+---+---
   |  7| 4 
2  |6 9|   
 8 |   | 7 

Run the class and choose and upload the file that you created in the above step. After a few moments you will be prompted to download a file which should contain the solution.

Happy cracking!

November 12, 2019

Creating and testing a strongly typed Stack class in X++

Filed under: 365 for Finance and Operations — Joe Brewer @ 12:47 pm

For my latest project in X++ I needed to use a Stack. .NET does already provide a Stack class however MS recommends using the Generics version as it is faster because it is typed. See Non-generic collections shouldn’t be used on GitHub. I had initially tried creating a Stack class using a container, but it was painfully slow – it took several minutes to push and pop 100,000 elements onto and off of the stack.

Here is my implementation:

class Stack extends List
{
    private Types dataType;

    public void push(anytype _value)
    {
        // double check that we have received a variable of the correct type
        if (typeOf(_value) != dataType)
        {
            throw error("Incorrect data type");
        }

        this.addStart(_value);
    }

    public void new(Types _type)
    {
        dataType = _type;

        super(_type);
    }

    public anytype pop()
    {
        ListIterator iterator;
        anytype topValue;

        // double check that there is an element on the stack
        if (!this.elements())
        {
            return null;
        }

        iterator = new ListIterator(this);

        // position the iterator to the top
        iterator.more();

        // retreive the value
        topValue = iterator.value();

        // remove the element from the stack
        iterator.delete();

        // return the value
        return topValue;
    }
}

Here is a job to demonstrate how to use the class and to test the speed of pushing and popping 100,000 elements on to and off of the stack:

class StackTest
{        
    /// <summary>
    /// Runs the class with the specified arguments.
    /// </summary>
    /// <param name = "_args">The specified arguments.</param>
    public static void main(Args _args)
    {    
        System.Random random;
        System.Diagnostics.Stopwatch stopwatch;
        System.TimeSpan timeSpan;
        Stack numberStack;
        str elapsedTime;

        random = new System.Random();
        numberStack = new GWStack(Types::Integer);
        stopwatch = new System.Diagnostics.Stopwatch();

        stopwatch.Start();

        // push 100,000 random numbers onto the stack
        for (int i = 0 ; i <= 100000 ; i++)
        {
            numberStack.push(random.Next(1, 1000));
        }

        // pop everything off of the stack one by one
        for (int i = 0 ; i <= 100000 ; i++)
        {
            numberStack.pop();
        }

        stopwatch.Stop();
        timeSpan = stopwatch.Elapsed;

        elapsedTime = System.String::Format("{0:00}:{1:00}:{2:00}.{3:0000}",
            timeSpan.Hours, timeSpan.Minutes, timeSpan.Seconds,
            timeSpan.Milliseconds);

        info(strFmt("elapsed time for list based stack: %1", elapsedTime));
    }

}

The results show that pushing and popping 100,000 elements is well under 1 second.

Let me know if you use it or if you found this helpful.

« Newer Posts

Powered by WordPress