Saturday, 9 August 2025

Get the last workflow comment

The code snippet below retrieves the last workflow comment of a workflow using the workflow correlation ID.

One of my customers requested a custom workflow with a separate approval e-mail message in Outlook using a custom template in F&O. In the email message, they wanted the full details from F&O including the comment of the person who submitted the workflow for approval.

To answer a question about this on the Microsoft F&O forum, I found this code in my code archive and thought it might be worth sharing.

public static str getLastTrackingComment(WorkflowCorrelationId _workflowCorrelationId)
{
    WorkflowTrackingTable           trackingTable;
    WorkflowTrackingCommentTable    trackingCommentTable;

    if (_workflowCorrelationId)
    {
        trackingTable = Workflow::findLastWorkflowTrackingRecord(_workflowCorrelationId);

        if (trackingTable)
        {
            trackingCommentTable = WorkflowTrackingCommentTable::findTrackingRecId(trackingTable.RecId);
        }
    }

    return trackingCommentTable.Comment;
}

Sunday, 13 July 2025

Use a custom form as a dialog in a custom runnable class

Sometimes there is a requirement to perform a simple operation in Dynamics 365 Finance and Operations using some limited input from the user before the operation is executed.

A real world example of custom records and a custom form to view, edit and process "records" Due to customer privacy, I can't use the real names of the records and will just refer to the data a "record". The requirement is to update a single field of a read only record. The record is not allowed to be edited in the form because of its status. In the form datasource active method, there is code to check the status of the record and sets the datasource allowEdit property according to the status of the record. However, due to certain factors, sometimes the record is not processed in time and the date of the record is not current anymore, it's now in the past. This creates a catch 22. This record cannot be processed because the date is in the past and the date cannot be updated because it's read only due to the status. The solution is, the clicks on a menu item button on the form to change the date of the record. When the user click the menu item button, a dialog is displayed showing a date field. The use can only choose a date in the future. The validation on the form makes sure a valid date is selected when the user clicks in the OK button.

This article recommends some basic level of code knowledge and desing patterns and doesn't go into detail and inner workings of the code. However, it demonstrates a simple pattern for a runnable class using a custom form dialog.

Recommended code knowlege:

  • You know what a runnable class is
  • You have basic knowledge of how the runnable class pattern works
  • You know how to create a form of type dialog and know ho to add input fields on the form
  • You know how to add validation on the form to validate the user input data
  • You know how to ge variable values in code from a form object of the input fields of the form

What you need to do to implement the pattern (in a nutshell):

  • Create your own runnable class with a static main method, run and prompt methods
  • Create your own form (type dialog) with required input fields on the form.
  • Add an OK button to the form that calls the forms closeOK method.
  • Add an Cancel button to the form that just closes the form whithout doing anything
  • Copy the code below to your own runnable class.
  • Change the class name on the main method to your own class
  • Change the form name in the prompt method to your own own form name
  • Retrieve the input values from the form object in the prompt method if user has clicked the OK button
  • Finally, implement the business logic in the run method

Runnable class code

public final class MyCustomRunnableClassWithCustomFormAsDialog
{
    private FormRun mFormRun;
    //add input form class variables
    
    public void run()
    {
        //add logic here.
    }
    
    public boolean prompt()
    {
        Object obj;
        
        mFormRun = classFactory.formRunClass(new Args(formStr(MyCustomFormDialog))); // Change form name.
        mFormRun.init();
        mFormRun.run();
        mFormRun.wait();
        
        if (mFormRun.closedOk()) //did the user click OK?
        {
            obj = mFormRun;
            //retrieve form variables values from form obj and set class variable here.
        }
        
        return mFormRun.closedOk();
    }
    
    public static void main(Args _args)
    {
        MyCustomRunnableClassWithCustomFormAsDialog operation = new MyCustomRunnableClassWithCustomFormAsDialog();
        
        if (operation.prompt())
        {
            operation.run();
        }
    }    
}

Saturday, 12 July 2025

Extended Data Types

Background

This is another article I wrote ages ago that was originally published on Axaptapedia, which no longer exists. I found a copy online thought it was worth sharing as a blog as most of it remains relevant for Dynamics 365 Finance and Operations.

I have not edited the content but I left out a part about creating EDTs in code by an another author who added that later.

Side note: An extended data type is also referred to a an EDT.

Okay, enough of that. Let's start!

Introduction

Extended data types are very important and if used correctly, very powerful. An extended data type is a user-defined definition of a primitive data type. The following primitive data types can be extended: boolean, integer, real, string, date and container.

Inheritance

Name is a standard string EDT that represents the name of a person, company or object. If a property on Name is changed, all EDT's that inherit from it will automatically reflect the changed value. For example if the standard length is not long enough, it can be increased and all child EDT's will automatically be increased along with it. All database fields, forms and reports where the EDT is used, will also reflect the changed property.

Properties

Some of the properties that can be modified are StringSize, Alignment, DisplayLength, Label and HelpText.

Number sequences

When creating a number sequence, an extended data type is required to associate the number sequence with.

Advantages

1. Consistency in data model, forms and reports.

2. Automatic lookups (if table relation property is set).

3. Improve readability in code.

Sunday, 29 June 2025

Use PowerShell to search for labels fast

Background

Here is a quick and simple method I use a lot to search for labels using PowerShell. It's seriously fast!

Let's start.

1. Open PowerShell and change the current directory to your LocalPackages folder. Change the path according to your system. Make sure you have enough permission on the local packages directory. This is why I always open PowerShell as Administrator.

cd C:\Users\mrbean\AppData\Local\Microsoft\Dynamics365\10.0.1935.92\PackagesLocalDirectory

2. Let's search for the label "Invoice" using the PowerShell "findstr" command. The command below will search in all the en-US label files.

findstr /s /l /i /n /E /c:"=Invoice" *en-US.label.txt

Explanation of the command arguments:

  • /s: Searches subdirectories.
  • /l: Performs a literal search (case-sensitive by default).
  • /i: Makes the search case-insensitive.
  • /n: Displays line numbers in results.
  • /E: Matches the pattern at the end of a line.
  • /c:"=Invoice": Searches for the exact string =Invoice.
  • *en-US.label.txt: Targets all files with the en-US.label.txt extension.

Note: The "/E" argument in the PowerShell command returns all lines of text that end with "Invoice". Because we are dealing with Dynamics 365 Finance label files that have and ID and label text separated with an "=" I've included "=" in the search. The causes the command to return the exact labels we search for. Copy and paste the label ID from the results.



Thursday, 29 May 2025

Methods as table fields in workflow designer

Background

The problem is that by default, only table fields are visible in the workflow designer and sometimes it is required to use business logic to drive the workflow.

The solution to this problem is to add custom parm methods to the workflow document class of the workflow in question. The example below is from one of my custom workflows. I have simplified the code a bit for this example.

If you are working with a standard workflow, you could add your own parm methods in a class extension of the workflow document class.

The parm methods must have three arguments. In the example below, the workflow is based on SalesTable (don't ask ne why). So the TableId and RecId passed to the parm method by the system is related to the current SalesTable record. You will have to modify this to find the related record in your workflow of course.

Note:

  • Add methods to the workflow document class (class extending WorkflowDocument).
  • The parm method must have three arguments: CompanyId, TableId and RecId.
  • The method name must start with "parm".

Example


    public class MyApprovalWorkflowDocument extends WorkflowDocument
    {
        public MyEnum parmHasCreditLine(CompanyId _companyId, TableId _tableId, RecId _recId)
        {
            SalesTable salesTable;
            MyEnum hasCreditLine; //custom base NoYes enum with label which will be visible in the workflow designer

            changeCompany(_companyId)
            {
                salesTable = SalesTable::findRecId(_recId);

                if (salesTable)
                {
                    if (salesTable.type().myHasCreditLine())
                    {
                        hasCreditLine = MyEnum::Yes;
                    }
                    else
                    {
                        hasCreditLine = MyEnum::No;
                    }
                }
            }

            return hasCreditLine;
        }
    }

Monday, 22 July 2024

RunBaseBatch

Background

This is an article I wrote ages ago, originally published on Axaptapedia, which no longer exists. I found the original in my archive and thought it was worth sharing as a blog, as most of it remains relevant for Dynamics 365 Finance and Operations. I've made minor edits and removed deprecated elements from the original, written for Axapta 4.0.

Okay, enough of that. Let's start!

Introduction

The RunBaseBatch class extends the RunBase class and allows creating classes (jobs) that can be added to the batch queue. Once added, the batch server executes the job, a process known as batch processing.

Input Data Parameters

Typically, a class contains logic without a graphical user interface. As RunBaseBatch inherits from RunBase, it uses the dialog framework to prompt users for input parameters interactively. These parameters are class member variables added to the dialog box. The control type depends on the extended data type used. For example, properties like Auto, Combo box, or Radio button can be set on Base enums via the Extended Data Type.

A boolean variable creates a checkbox, an enum a combo box, and a string a text edit control. Each dialog control corresponds to a class variable used to set and retrieve values, persisting them for batch execution. Controls are usually added in code during runtime, requiring a different approach to override methods compared to standard forms. Alternatively, an existing dialog from the AOT can be used for greater control, similar to a standard form.

Parameters enhance job flexibility and reusability, allowing different behaviors based on selections. For instance, a log cleanup job might accept a "days" parameter to delete records older than a user-specified value (e.g., 30 days).

Persistence

Parameters, as class variables, are persisted in the database via the SysLastValue framework. They are packed when the job is queued and unpacked when executed by the batch server. Variables must be listed in a local macro, and the class must override the pack and unpack methods, as RunBaseBatch implements the SysPackable interface.

The pack method is called when a job is queued, and unpack before execution. During batch execution, only new, unpack, and run methods are called, as no user interaction occurs. The run method should contain the core logic.

Query

RunBaseBatch supports queries for data selection. If the queryRun method is overridden, the dialog automatically displays query ranges and values. Users can modify the query via a "Select" button, adding or removing ranges, joining related tables, or changing sort order. For reusable queries, create them in the AOT rather than in code.

Operation Progress

For time-consuming operations, a progress bar is recommended. The RunBase framework's Operation Progress functionality, inherited by RunBaseBatch, can be implemented in the run method to display progress.

Making the Class Runnable

A class is not executable by default. To make it runnable, add a static main method with an Args parameter. The kernel calls this method interactively, passing an Args object containing properties like an active buffer or caller reference. This supports constructor-based object-oriented design and enables access via menu items from the main menu or forms.

Considerations


New Method

As RunBaseBatch classes can run in batch journals, the new method should have no arguments to avoid stack trace errors during batch journal setup.

Version Number

Class variables are listed in a local macro for persistence. A version number tracks changes to the variable list, starting at 1 and incrementing with changes. The unpack method must handle versioning to prevent errors when the persisted variable list size mismatches the macro list.

Methods


new

Creates a new class instance. Called during batch execution.

pack

Persists class variables using a local macro. Called when the dialog is closed with "OK".

unPack

Re-instantiates class variables from the macro. Called before run in batch execution.

dialog

Adds controls to the dialog box, setting values from corresponding class variables.

getFromDialog

Assigns class variable values from dialog controls.

validate

Validates input parameters, displaying an info log for invalid inputs.

run

The central method containing the class's core logic. Called during batch execution.

initParmDefault

Initializes macro list variables when unPack returns false (no usage data found).

queryRun

Returns a queryRun object if the class uses a query.

canGoBatchJournal

Indicates if the class can be included in a batch journal. Default is false.

canGoBatch

Indicates if the class can run in batch. Adds a "Batch" tab to the dialog if true. Default is true.

runsImpersonated

Determines if the job runs under the submitting user’s or batch user’s account. Uses runAs if true. Default is false.

description (static)

Returns the class description, displayed in the batch list and dialog title.

caption

Overrides the base class description for the batch list and dialog title.

Creating a Basic Batch Job Step by Step


Create a Class

From the AOT, create a class extending RunBaseBatch:

public class AV_RunbaseBatchDemo extends RunBaseBatch
{
}

Class Declaration

Add the following to the class declaration:

public class AV_RunbaseBatchDemo extends RunBaseBatch
{
    NoYes displayMessage;
    Description message;

    DialogField fieldDisplayMessage;
    DialogField fieldMessage;

    #define.CurrentVersion(1)

    #localmacro.CurrentList
        displayMessage,
        message
    #endmacro
}

Override the pack Method

public container pack()
{
    return [#CurrentVersion, #CurrentList];
}

Override the unpack Method

public boolean unpack(container _packedClass)
{
    Integer version = RunBase::getVersion(_packedClass);

    switch (version)
    {
        case #CurrentVersion:
            [version, #CurrentList] = _packedClass;
            break;

        default:
            return false;
    }

    return true;
}

Create the description Method

This sets the batch queue description and dialog caption:

public static ClassDescription description()
{
    return 'Runbase batch demo';
}

Create the construct Method

public static AV_RunbaseBatchDemo construct()
{
    return new AV_RunbaseBatchDemo();
}

Override the initParmDefault Method

public void initParmDefault()
{
    super();
    displayMessage = NoYes::Yes;
    message = 'Hello World!';
}

Override the dialog Method

protected Object dialog()
{
    DialogRunbase dlg;

    dlg = super();
    fieldDisplayMessage = dlg.addFieldValue(typeId(NoYes), displayMessage, 'Display message', 'Indicates whether to display a message or not.');
    fieldMessage = dlg.addFieldValue(typeId(Description), message, 'Message', 'The message to display');
    return dlg;
}

Override the getFromDialog Method

public boolean getFromDialog()
{
    boolean ret;

    ret = super();
    
    displayMessage = fieldDisplayMessage.value();
    message = fieldMessage.value();
    
    return ret;
}

Override the run Method

public void run()
{
    if (displayMessage)
    {
        info(message);
    }
}

Create the static main method to make the class runnable.

public static void main(Args _args)
{
    AV_RunbaseBatchDemo rb = AV_RunbaseBatchDemo::construct();

    if (rb.prompt())
    {
        rb.run();
    }
}

Tuesday, 16 July 2024

How to change the default model in Visual Studio

Background

When creating new projects in Visual Studio, the model of the project defaults to the Fleet Management model. This is annoying because you always have to manually change the model of the project to your own model before you can start working.


Perhaps even more annoying is that it's not possible to change the default model in the Visual Studio Dynamics 365 Options Add-In.


Lucky for us there's an easy fix for this by simply editing the DynamicsDevConfig.xml file located on the Dev Virtual Machine.

Here's how to change the default model in Visual Studio on your Dev Virtual Machine.


1. Close Visual Studio.

2. Locate the DynamicsDevConfig.xml file in C:\Users\\Documents\Visual Studio 365\.

3. Change the DefaultModelForNewProjects value in DynamicsDevConfig.xml to preferred model.

4. Launch Visual Studio and create a new project to check if it's working.