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.
Last updated: September 2026

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 void return type and an empty method body (delegate void myDelegate(...) {}) and invokes it at specific execution points. Subscribers listen by implementing a public static void method decorated with [SubscribesTo(classStr(Publisher), delegateStr(Publisher, DelegateName))]. Because delegates cannot return values directly, bidirectional communication requires passing a mutable EventHandlerResult object (_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:

  1. Return Type: Must strictly return void.
  2. Empty Body: Must have an empty method body consisting solely of curly brackets {}.
  3. 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:

  1. Must be declared as public static void.
  2. Must match the delegate's parameter signature exactly.
  3. 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, use formStr(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 checks result.hasResult() and extracts result.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 DimensionChain of Command (CoC)Delegate Architecture
Execution PatternNested, sequential onion wrapping around entire methodPoint-in-time broadcast notification at a specific code location
Subscription CardinalitySequential chain across models (one wrapper per model)True 1-to-Many Pub/Sub (unlimited independent subscribers)
Invocation OrderNested pipeline; pre-logic inward, post-logic outwardNon-deterministic; subscribers execute in arbitrary order
Access to Instance ContextDirect access to this instance methods and table fieldsLimited strictly to parameters explicitly passed to the delegate
Base Logic SuppressionForbidden; next call is strictly mandatoryAllowed; publisher can conditionally bypass code if hasResult() is true
Return ValuesDirectly returns typed value from next invocationMust return void; state returned via EventHandlerResult
Class Definitionpublic final class with [ExtensionOf]public static void methods with [SubscribesTo]
Primary Use CaseAugmenting standard CRUD methods, form behaviors, and transactionsPoint-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

  1. 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;
        }
    }
    
  2. 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]).

Loading diagram...
Delegate Bidirectional Communication Pipeline with EventHandlerResult
Test Your Knowledge

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
B
C
D
Test Your Knowledge

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?

A
B
C
D
Test Your Knowledge

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?

A
B
C
D
Test Your Knowledge

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?

A
B
C
D