9.4 Event Handler Classes & Delegate Architecture
Key Takeaways
- Delegates in X++ implement the Publisher-Subscriber pattern, enabling a publisher class to broadcast events to multiple independent subscriber methods without introducing compile-time dependencies.
- A delegate declaration must always return void, must have an empty method body ({}), and is raised by the publisher using standard method call syntax.
- Delegate subscribers must be implemented as public static void methods decorated with the [SubscribesTo(classStr(Publisher), delegateStr(Publisher, DelegateName))] attribute.
- Because delegates return void, returning state or computed values from a subscriber back to the publisher requires passing a mutable result object, typically EventHandlerResult (_result.result(...)).
- While Chain of Command provides sequential, nested wrapping with direct access to instance context, delegates provide decoupled point-in-time notifications suitable for multi-subscriber broadcasting.
9.4 Event Handler Classes & Delegate Architecture
Quick Answer: Delegates in X++ provide a loosely coupled Publisher-Subscriber (Pub/Sub) event mechanism. A publisher class declares a delegate with a
voidreturn type and an empty method body (delegate void myDelegate(...) {}) and invokes it at specific execution points. Subscribers listen by implementing apublic static voidmethod decorated with[SubscribesTo(classStr(Publisher), delegateStr(Publisher, DelegateName))]. Because delegates cannot return values directly, bidirectional communication requires passing a mutableEventHandlerResultobject (_result.result(value)). While Chain of Command (CoC) wraps entire method lifecycles sequentially, Delegates broadcast point-in-time notifications to multiple independent subscribers simultaneously.
1. Delegate Architecture and Event-Driven Design in X++
In complex enterprise ERP architectures, tightly coupling business logic across different domain modules creates maintainability bottlenecks. For instance, when posting a general ledger journal, tax calculations, warehouse reservations, and external regulatory logging may need to respond without the posting class needing hard compile-time references to those disparate subsystems.
X++ implements the Publisher-Subscriber pattern through Delegates:
- Publisher: Defines and raises the delegate event when a specific milestone occurs. The publisher has zero compile-time awareness of who is listening.
- Subscribers: External event handler classes that attach to the delegate. When the publisher raises the event, the runtime invokes all subscribed handler methods.
Delegate Pub/Sub Architecture
┌────────────────────────────────────────────────────────┐
│ Publisher Class (e.g., SalesLineType) │
│ 1. Instantiates EventHandlerResult result │
│ 2. Raises delegate: this.calculatingDiscount(..., res)│
└───────────────┬────────────────────────┬───────────────┘
│ Broadcasts Notification│
┌────────┴────────┐ ┌────────┴────────┐
▼ ▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Subscriber 1 │ │ Subscriber 2 │ │ Subscriber 3 │ │ Subscriber 4 │
│ ISV Loyalty │ │ Tax Engine │ │ Partner Audit│ │ Regulatory │
│ Discount │ │ Calculation │ │ Logging │ │ Compliance │
└──────────────┘ └──────┬───────┘ └──────────────┘ └──────────────┘
│ Sets _result.result(calculatedVal)
▼
┌────────────────────────────────────────────────────────┐
│ Publisher Class resumes execution: │
│ 3. Checks if (result.hasResult()) │
│ 4. Consumes returned value in downstream posting │
└────────────────────────────────────────────────────────┘
2. Declaring and Invoking Delegates in Publisher Classes
Delegates are declared as members of classes, tables, or forms. They must follow strict syntactic requirements:
- Return Type: Must strictly return
void. - Empty Body: Must have an empty method body consisting solely of curly brackets
{}. - Parameters: Can accept any primitive types, table buffers, class objects, or event result containers.
Publisher Declaration and Invocation Syntax
public class SalesPriceCalculator
{
// 1. Delegate Declaration (Must return void and have an empty body)
delegate void pricingCalculated(SalesLine _salesLine, EventHandlerResult _result)
{
}
public AmountCur calculateLinePrice(SalesLine _salesLine)
{
AmountCur standardPrice = _salesLine.QtyOrdered * 100.00;
// 2. Prepare state container for bidirectional communication
EventHandlerResult result = new EventHandlerResult();
// 3. Raise the delegate
this.pricingCalculated(_salesLine, result);
// 4. Check if an external subscriber provided an override
if (result.hasResult())
{
AmountCur customPrice = result.result();
return customPrice;
}
return standardPrice;
}
}
[!NOTE] If no subscribers are listening when
this.pricingCalculated(...)is called, the execution simply traverses the empty method body and resumes immediately without error.
3. Subscribing to Delegates with Event Handler Classes
Subscribers reside in dedicated event handler classes. A subscriber method must adhere to three rigid rules:
- Must be declared as
public static void. - Must match the delegate's parameter signature exactly.
- Must be decorated with the
[SubscribesTo]attribute.
Subscriber Implementation Syntax
public final class ABC_PricingEventHandler
{
[SubscribesTo(classStr(SalesPriceCalculator), delegateStr(SalesPriceCalculator, pricingCalculated))]
public static void onPricingCalculated(SalesLine _salesLine, EventHandlerResult _result)
{
// Check customer-specific contract pricing
if (_salesLine.CustAccount == 'US-001')
{
AmountCur negotiatedPrice = _salesLine.QtyOrdered * 85.00;
// Return the computed price back to the publisher
_result.result(negotiatedPrice);
}
}
}
Attribute Breakdown: [SubscribesTo]
classStr(PublisherClass): Intrinsic function specifying the class declaring the delegate.delegateStr(PublisherClass, DelegateName): Intrinsic function specifying the exact delegate name.- If subscribing to a table delegate, use
tableStr(TableName). If subscribing to a form delegate, useformStr(FormName).
4. Returning State and Bidirectional Communication: EventHandlerResult
Because multiple subscribers can listen to a single delegate simultaneously, a delegate cannot have a return type like AmountMST or boolean. If it did, the runtime would have no mechanism to determine which subscriber's return value takes precedence.
To pass computed values or decision states back to the publisher, X++ provides specialized result classes:
EventHandlerResult: Passes arbitrary object or primitive results back using_result.result(myValue). The publisher checksresult.hasResult()and extractsresult.result().EventHandlerAcceptResult: Specifically designed for boolean validation decisions (accept/reject). Subscribers call_result.accept(false)to reject an operation.
// Publisher
EventHandlerAcceptResult acceptResult = new EventHandlerAcceptResult();
this.validateCreditLimit(custTable, acceptResult);
if (!acceptResult.isAccepted())
{
throw error("Credit validation failed by external subscriber.");
}
// Subscriber
[SubscribesTo(classStr(CreditManager), delegateStr(CreditManager, validateCreditLimit))]
public static void onValidateCredit(CustTable _cust, EventHandlerAcceptResult _result)
{
if (_cust.Blocked != CustVendorBlocked::No)
{
_result.accept(false); // Reject
}
}
5. Architectural Comparison: Delegates vs. Chain of Command (CoC)
Choosing between Chain of Command and Delegates is a core competency tested on the MB-500 exam.
| Architectural Dimension | Chain of Command (CoC) | Delegate Architecture |
|---|---|---|
| Execution Pattern | Nested, sequential onion wrapping around entire method | Point-in-time broadcast notification at a specific code location |
| Subscription Cardinality | Sequential chain across models (one wrapper per model) | True 1-to-Many Pub/Sub (unlimited independent subscribers) |
| Invocation Order | Nested pipeline; pre-logic inward, post-logic outward | Non-deterministic; subscribers execute in arbitrary order |
| Access to Instance Context | Direct access to this instance methods and table fields | Limited strictly to parameters explicitly passed to the delegate |
| Base Logic Suppression | Forbidden; next call is strictly mandatory | Allowed; publisher can conditionally bypass code if hasResult() is true |
| Return Values | Directly returns typed value from next invocation | Must return void; state returned via EventHandlerResult |
| Class Definition | public final class with [ExtensionOf] | public static void methods with [SubscribesTo] |
| Primary Use Case | Augmenting standard CRUD methods, form behaviors, and transactions | Point-in-time lifecycle events, multi-party notifications, pluggable calculations |
6. Scenario Walk-Through: Dynamic Freight Engine via Delegate
Scenario Description
An enterprise distributor has a standard logistics class InventShipmentProcessor. During shipment finalization, freight charges must be calculated. By default, standard shipping applies. However, third-party logistics ISV packages or partner customizations must be able to supply custom carrier freight rates without modifying InventShipmentProcessor.
Implementation Walkthrough
- Publisher Class Definition (
InventShipmentProcessor):public class InventShipmentProcessor { // Declare the delegate hook delegate void calculateFreightRate(InventShipmentTable _shipment, EventHandlerResult _result) { } public AmountMST finalizeShipment(InventShipmentTable _shipment) { AmountMST freightRate = 50.00; // Base standard freight EventHandlerResult result = new EventHandlerResult(); this.calculateFreightRate(_shipment, result); if (result.hasResult()) { freightRate = result.result(); // Use external subscriber rate } info(strFmt("Shipment %1 finalized with freight: %2", _shipment.ShipmentId, freightRate)); return freightRate; } } - ISV Subscriber Implementation (
FedExCarrierIntegration):public final class FedExCarrierIntegration { [SubscribesTo(classStr(InventShipmentProcessor), delegateStr(InventShipmentProcessor, calculateFreightRate))] public static void onCalculateFreight(InventShipmentTable _shipment, EventHandlerResult _result) { if (_shipment.CarrierId == 'FEDEX') { AmountMST apiRate = 72.50; // Fetched from FedEx API _result.result(apiRate); } } }
7. Real-World Exam Traps: Event Handlers & Delegates
[!WARNING] Exam Trap 1: Attempting to Declare a Delegate with a Non-Void Return Type An exam question asks which delegate declaration syntax is valid:
delegate AmountMST calculateDiscount(...)ordelegate void calculateDiscount(...). In X++, delegates must always returnvoid. The compiler rejects any delegate returning a non-void data type.
[!WARNING] Exam Trap 2: Implementing Logic Inside a Delegate Declaration Writing code statements inside a delegate method's body in the publisher class triggers a compile-time error. Delegate declarations must have an empty body
{}.
[!WARNING] Exam Trap 3: Creating an Instance Method as a Delegate Subscriber Declaring a delegate subscriber method as
public void mySubscriber(...)without thestatickeyword fails compilation. Delegate event handlers must be declared aspublic static void.
[!WARNING] Exam Trap 4: Attempting to Call
nextInside an Event Handler Method Candidates frequently confuse Chain of Command with Event Handlers. You never callnextinside an event handler method ([SubscribesTo]). Callingnextis strictly reserved for Chain of Command extension methods ([ExtensionOf]).
A developer is declaring a custom delegate on a base business class to allow external models to participate in tax calculations. What are the strict syntax requirements for declaring a delegate in X++?
A developer needs to subscribe to a delegate named calculatingCommission declared on the SalesOrderTotals class. How must the subscriber method be implemented to compile successfully?
When a publisher class calls a delegate that returns void, how can a subscriber method pass a calculated discount amount back to the publisher for use in the remaining business process?
An architect is evaluating whether to implement a customization using Chain of Command (CoC) or a Delegate. Which scenario strictly mandates the use of Chain of Command rather than a Delegate?