Mastering the Command Pattern in Dynamics 365 F&O: A Step-by-Step Guide


What is the Command Pattern?
The Command pattern transforms a request into a stand-alone object containing all information about the request. Think of it like a restaurant:
Waiter takes the order without knowing how to cook
Order ticket contains all details
Chef knows how to prepare the food
This separation allows each part to focus on its responsibility, making the system more flexible and maintainable.
Generic Command Pattern Structure
Command Interface: Defines method(s) for executing operations
Concrete Command: Implements the interface and connects to specific actions
Invoker: Asks the command to execute
Receiver: Performs the actual operations
Now D365 F&O
In D365 F&O, without the Command pattern, we often see code like this:
// Direct implementation in button click handler
public void VoP()
{
super();
// Get selected statements
List<BankStatement> statementLines = formView.getSelectOpenStatementList();
BankStmtISOReportEntry bankStmtISOReportEntry = formView.getSelectOpenStatement();
BankStmtISOAccountStatement accountStatement = BankStmtISOAccountStatement::find(
bankStmtISOReportEntry.BankStmtISOAccountStatement);
// Create arguments for the dialog
Args args = new Args();
args.caller(element);
args.record(accountStatement.getBankAccountTable());
args.parmObject(statementLines);
// Run the voucher generation menu function
new MenuFunction(menuitemDisplayStr('BankAutomationStatementGenerateVoucherDialog'),
MenuItemType::Display).run(args);
}
// Similar implementation with slight differences
public void clickedForPayment()
{
super();
// Get selected statements (duplicate code)
List<BankStatement> statementLines = formView.getSelectOpenStatementList();
BankStmtISOReportEntry bankStmtISOReportEntry = formView.getSelectOpenStatement();
BankStmtISOAccountStatement accountStatement = BankStmtISOAccountStatement::find(
bankStmtISOReportEntry.BankStmtISOAccountStatement);
// Create arguments for the dialog (duplicate code)
Args args = new Args();
args.caller(element);
args.record(accountStatement.getBankAccountTable());
args.parmObject(statementLines);
// Run the payment generation menu function
new MenuFunction(menuitemDisplayStr('BankStatementGeneratePaymentDialog'),
MenuItemType::Display).run(args);
}
Problems: Code duplication, poor maintainability, tight coupling, and limited extensibility.
Command Pattern Implementation
interface IBankStatementCommand
{
void execute();
}
/// <summary>
/// Base command for bank statement operations
/// </summary>
class BankStatementCommandBase implements IBankStatementCommand
{
protected FormStringControl element;
protected FormDataSource formView;
protected str menuItemName;
/// <summary>
/// Constructor
/// </summary>
public BankStatementCommandBase(FormStringControl _element, FormDataSource _formView, str _menuItemName)
{
element = _element;
formView = _formView;
menuItemName = _menuItemName;
}
/// <summary>
/// Executes the command
/// </summary>
public void execute()
{
// Get selected statements
List<BankStatement> statementLines = formView.getSelectOpenStatementList();
BankStmtISOReportEntry bankStmtISOReportEntry = formView.getSelectOpenStatement();
BankStmtISOAccountStatement accountStatement = BankStmtISOAccountStatement::find(
bankStmtISOReportEntry.BankStmtISOAccountStatement);
// Create arguments for the dialog
Args args = new Args();
args.caller(element);
args.record(accountStatement.getBankAccountTable());
args.parmObject(statementLines);
// Run the menu function
new MenuFunction(menuitemDisplayStr(menuItemName), MenuItemType::Display).run(args);
}
}
class GenerateVCommand extends BankStatementCommandBase
{
/// <summary>
/// Constructor
/// </summary>
public GenerateVCommand(FormStringControl _element, FormDataSource _formView)
{
super(_element, _formView, 'BankAutomationStatementGenerateVoucherDialog');
}
}
class GeneratePaymentCommand extends BankStatementCommandBase
{
/// <summary>
/// Constructor
/// </summary>
public GeneratePaymentCommand(FormStringControl _element, FormDataSource _formView)
{
super(_element, _formView, 'BankStatementGeneratePaymentDialog');
}
}
/// <summary>
/// Handles button click for voucher generation
/// </summary>
public void VoP()
{
super();
// Create and execute the voucher command
IBankStatementCommand command = new GenerateVCommand(element, formView);
command.execute();
}
/// <summary>
/// Handles button click for payment generation
/// </summary>
public void clickedForPayment()
{
super();
// Create and execute the payment command
IBankStatementCommand command = new GeneratePaymentCommand(element, formView);
command.execute();
}
Note: For the sake of clarity and to focus on understanding the Command pattern, I've presented all code components together in this article. In a real D365 F&O implementation, you would typically organize these components following platform conventions, with separate files for interfaces, base classes, and concrete implementations placed in appropriate folders within your model (Which i pointed below).
Implementation Architecture
Key Benefits in D365 F&O
Eliminated code duplication*: Common logic in one place*
Improved maintainability*: Changes happen in a single location*
Decoupled UI from business logic*: Click handlers just create and execute commands*
Enhanced extensibility*: Add new commands without modifying existing code*
Adding New Functionality Is breeze
class ReconcileStatementCommand extends BankStatementCommandBase
{
public ReconcileStatementCommand(FormStringControl _element, FormDataSource _formView)
{
super(_element, _formView, 'BankStatementReconcileDialog');
}
}
public void clickedForReconcile()
{
super();
IBankStatementCommand command = new ReconcileStatementCommand(element, formView);
command.execute();
}
That's it! No modification to existing code required.
For organizing the Command pattern implementation in D365 F&O, here's a possible folder structure that follows platform best practices
When to Use in D365 F&O
Use the Command pattern when:
Multiple UI elements trigger similar but different operations
You want to decouple UI events from business logic
You need to implement operations that can be undone
You're building batch operations or need operation history
Next time you face a situation with multiple similar but different operations in D365 F&O, consider the Command pattern for a cleaner, more maintainable solution.
✨ Happy {..}
Subscribe to my newsletter
Read articles from Challa directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
