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(...) or delegate void calculateDiscount(...). In X++, delegates must always return void. 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 the static keyword fails compilation. Delegate event handlers must be declared as public static void.
Warning
Exam Trap 4: Attempting to Call next Inside an Event Handler Method
Candidates frequently confuse Chain of Command with Event Handlers. You never call next inside an event handler method ([SubscribesTo]). Calling next is 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++?
The delegate must be declared as public static AmountMST and must call super() in its body.
The delegate must return a boolean indicating whether any subscribers handled the event.
The delegate can return any primitive type, provided it is marked with the [Subscribable] attribute.
The delegate must return void, must have an empty method body ({}), and is declared using the delegate keyword.
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?
As a public static void method decorated with the [SubscribesTo(classStr(SalesOrderTotals), delegateStr(SalesOrderTotals, calculatingCommission))] attribute
As an instance method decorated with [ExtensionOf(classStr(SalesOrderTotals))] calling next calculatingCommission()
As a protected void method implementing the ISalesCommissionSubscriber interface
As a private static method decorated with [SubscribesTo("SalesOrderTotals.calculatingCommission")]
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?
The subscriber writes the calculated amount to a static global cache variable in the Application object.
The publisher passes a mutable EventHandlerResult object as an argument to the delegate, and the subscriber calls _result.result(calculatedAmount).
The subscriber alters the base class instance variables directly using the this keyword.
The subscriber raises a secondary return delegate that sends the calculated value back across the network.
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?
The customization requires broadcasting an event to multiple independent third-party ISV packages simultaneously.
The customization must execute within an unhandled background batch task without user interface controls.
The customization must wrap an existing standard table method (such as insert or validateWrite) to inspect input parameters, execute base logic via next, and alter the return value.
The customization requires passing validation rejection state back through an EventHandlerAcceptResult object.
Sections you finish are checked off in the contents.