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