Study guide

9.4 Event Handler Classes & Delegate Architecture

Microsoft Dynamics 365: Finance and Operations Apps Developer Associate MB-500

Track your progress as you read

Finished sections get a check mark.

1 Chapter 1: Cloud & Ecosystem Architecture

1.1 Cloud vs On-Premises Architecture & Deployment Typologies 1.2 Microsoft Ecosystem Integration & Extensibility Boundaries 1.3 Lifecycle Services (LCS) & Implementation Portal

2 Chapter 2: ALM, Environments & Unified Developer Experience

2.1 Package Deployment & Servicing 2.2 Unified Developer Experience (UDE) & Power Platform Admin Center 2.3 Environment Management & Governance

3 Chapter 3: Visual Studio Development & Metadata

3.1 Visual Studio Extension Models & Packages 3.2 Application Explorer & Element Designers 3.3 Database Synchronization & Visual Studio Debugging

4 Chapter 4: Azure DevOps Version Control & CI/CD

4.1 Version Control Architecture: TFVC vs Git in F&O 4.2 Branching, Merging & Conflict Resolution 4.3 CI/CD Build Pipelines & Artifact Generation

5 Chapter 5: UI Elements: Forms, Menus & Labels

5.1 Form Design & Form Patterns 5.2 Menus, Menu Items & Navigation 5.3 Label Files & Localization

6 Chapter 6: Data Model: Tables & Relational Design

6.1 Table Design & Extension 6.2 Views, Queries & Maps 6.3 Data Entity Design & Modeling

7 Chapter 7: Classes, Attributes & AOT Modifiers

7.1 Class Architecture & Access Modifiers 7.2 Attributes & Reflection 7.3 Event Handlers & Pre/Post Method Events

8 Chapter 8: X++ Core Programming & CRUD Operations

8.1 X++ Syntax, Control Flow & Global Functions 8.2 Data Manipulation & CRUD Logic 8.3 Best Practice Rules & Compiler Diagnostics

9 Chapter 9: Advanced OOP, Chain of Command & Extensibility

9.1 OOP Principles & QueryBuilder Class 9.2 Chain of Command (CoC) Implementation 9.3 Extensibility Patterns & Non-Breaking Changes 9.4 Event Handler Classes & Delegate Architecture 9.5 Variable Scoping, Declaration Placement & Object Lifetime

10 Chapter 10: F&O Frameworks & SysTest Unit Testing

10.1 SysOperation Framework Architecture 10.2 Workflow Framework & SysExtensionSerializer 10.3 SysTest Framework & Test Explorer

11 Chapter 11: Reporting Architecture & SSRS Reports

11.1 Reporting Architecture & Datastores 11.2 Report Data Providers & Contracts 11.3 SSRS Report Design, Deployment & Security 11.4 Determining When to Use a Specific Reporting Tool or Platform

12 Chapter 12: Business Intelligence, Workspaces & Electronic Reporting

12.1 Electronic Reporting (ER) Framework 12.2 Power BI Integration & Analytical Workspaces 12.3 Workspaces, Tiles, Lists & KPIs 12.4 Excel Integration & Office App

13 Chapter 13: Integration Patterns & Service Endpoints

13.1 Integration Pattern Evaluation & API Selection 13.2 Custom Services & SOAP/REST Endpoints 13.3 Consuming External Services & REST/SOAP APIs

14 Chapter 14: Data Management Framework (DMF) & Batch Processing

14.1 DMF Concepts, Projects & Sequencing 14.2 Entity Change Tracking & Recurring Integrations 14.3 Composite Entities & Batch OData API 14.4 Monitoring Entity Status, Availability & Job Execution

15 Chapter 15: Power Platform, Business Events & Azure Integrations

15.1 Dual-Write & Virtual Entities 15.2 Business Events Framework 15.3 Azure Key Vault Integration & Secure Storage

16 Chapter 16: Role-Based Security & Extensible Data Security

16.1 Role-Based Security Architecture 16.2 Extensible Data Security (XDS) Policies 16.3 Security Enforcement & Best Practices

17 Chapter 17: Performance Optimization & Data Access Patterns

17.1 Table & Form Caching Mechanisms 17.2 Temporary Tables & Set-Based Operations 17.3 Query Tuning, Indexes & Concurrency 17.4 Diagnostics: Trace Parser, Async & Sandbox Frameworks 17.5 Tuning Variable Scope, Buffer Lifetime & Memory Pressure

9.4 Event Handler Classes & Delegate Architecture

Key Takeaways Explain This Topic AI

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:

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, 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 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] ).

Loading diagram... Delegate Bidirectional Communication Pipeline with EventHandlerResult