How often did you find yourself in a situation when you created a Dynamics 365 Finance and Operations customization and wanted to create a logic which can respond to changes or notifies your change to future extensions of your code?
By using delegates and event handlers, you can define and handle custom events that are triggered by actions or changes in your application, such as user interactions or changes to data. This allows your code to be more modular, reusable, and easier to maintain, as different parts of the application can interact with each other without having to know the implementation details of the other parts.
What are delegates and event handlers?
A delegate is a type of object that references a method. In other words, a delegate refers to a function that can be passed as an argument to another method, making it easy to write code that can be executed dynamically at runtime.
On the other hand, an event handler is a method that is executed in response to a specific event occurring in the application. For example, in D365FO, you can create an event handler for a button click event, and your code will be executed when the user clicks the button. Event handlers are useful because they allow you to create custom logic that can be executed when specific events occur in your application, making it possible to add custom functionality in the future.
Why would I use delegates and event handlers?
There are several reasons why you would use delegates and event handlers in D365FO applications:
- Dynamic behavior: Delegates allow you to pass a method as a parameter to another method, enabling you to write code that can be executed dynamically at runtime. This is useful when you want to allow users to define the behavior of the system in a flexible way or when you want to make it easy to add or modify the behavior of the system without having to change the code directly.
- Customization: Event handlers allow you to create custom logic that can be executed when specific events occur in your application. This enables you to add custom functionality to the system, such as capturing data from the user or triggering a specific action in response to an event.
- Reusability: Delegates and event handlers can be used in multiple places within your application, making it easier to reuse code and reducing the amount of time and effort required to maintain the codebase.
- Decoupling: Delegates and event handlers allow you to decouple different parts of your application, making it easier to change the behavior of one part of the system without affecting other parts. This can make it easier to maintain and modify your application over time.
Delegates in X++ must have the return type void, so we need to use objects (e.g. EventHandlerResult) as method parameters to return a result.
What about the traps using delegates and event handlers?
Delegates and event handlers in X++ can be used in various scenarios such as:
- To notify the subscriber on a change that has happened in your implementation.
- To request a change in a behavior of the code flow based on the event handlers result.
The first scenario is quite straight forward as we call the delegate with a fire and forget principle, and all the subscribers will get notified about the change.
However, in the second scenario we can quickly introduce a few traps. Let’s us now look the problem through an example.
We are creating a customization where we want to upload a file. We want others to extend our implementation by adding their own logic for file security checks before we store the uploaded file to D365FO, such as:
- Check the file with a virus scanner of your choice.
- Check if the file is on a whitelist file extension.
- Check if the file is on a blacklist file extension.
- Any other possible check you can imagine 😊.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 |
/// <summary> /// The event result when storing a file. /// </summary> public final class StoreFileEventHandlerResult extends EventHandlerResult { boolean canStoreFile; /// <summary> /// Can the file be stored. /// </summary> /// <param name = "_value">Can store a file to the system</param> /// <returns>True if the file can be store, otherwise false.</returns> public boolean canStoreFile(boolean _value = false) { if (!prmisdefault(_value)) { canStoreFile = _value; } return canStoreFile; } } /// <summary> /// A sample Upload file manager class. /// </summary> public class UploadFileManager { /// <summary> /// When a file is uploaded to the system. /// </summary> /// <param name = "_fileName">The file uploaded to the system</param> /// <param name = "_fileStream">The file content</param> public void fileUploaded(str _fileName, System.IO.Stream _fileStream) { // check the file for security issues StoreFileEventHandlerResult eventResult = new StoreFileEventHandlerResult(); // call the delegate this.onFileSecurityCheck(_fileName, _fileStream, eventResult); // logic to store the file to database if (eventResult.canStoreFile() == true) { // should be here ;) } } /// <summary> /// Request external file security check. /// </summary> /// <param name = "_fileName">The file uploaded to the system</param> /// <param name = "_fileStream">The file content</param> /// <param name = "_eventResult">The result of file security check</param> delegate void onFileSecurityCheck(str _fileName, System.IO.Stream _fileStream, EventHandlerResult _eventResult) { } } public class SubscriberOne { [SubscribesTo(classStr(UploadFileManager), staticDelegateStr(UploadFileManager, onFileSecurityCheck))] public void checkWithVirusScanner(str _fileName, System.IO.Stream _fileStream, EventHandlerResult _eventResult) { // calling the virus scanner _eventResult.canStoreFile(isFileInfected(_fileStream) == false); } } public class SubscriberTwo { [SubscribesTo(classStr(UploadFileManager), staticDelegateStr(UploadFileManager, onFileSecurityCheck))] public void checkFileIsWhitelisted(str _fileName, System.IO.Stream _fileStream, EventHandlerResult _eventResult) { // get the file extension str fileExtension = System.IO.Path::GetExtension(_fileName); // check for whitelisting if (fileExtension == '.docx') { _eventResult.canStoreFile(true); } // other files are blocked _eventResult.canStoreFile(false); } } |
Our code above shows how simple it is to use delegates and event handlers and the code is totally legit.
So, what is the problem with our implementation:
- We have more than one subscriber doing different file security checks and each of them can return a different result.
- Each of the subscriber will return a file security check result and the last one called will win, and the result from the previous subscribers will be overwritten.
- We used a principle of method return value, which is a bad coding pattern when writing event handlers as we are not in control of the resulted value.
Now that we are aware of the traps, let us look at a possible scenario that can solve the above problem.
The Solution: Ensuring only one response
A better approach is to create an object and pass it as a parameter to the delegate. This way you can implement a class which can prevent setting multiple responses.
Lucky for us 😊, D365FO already has a mechanism in place to prevent the unexpected behavior with the so-called EventHandlerResult class. The class has an additional static constructor called newSingleResponse(), which ensures that the logic fails if more than one subscriber provides a result with the message ‘Only a single subscriber is allowed to respond with a result’.
In the following example we will be using a simple implementation of EventHandlerResult class called EventHandlerRejectResult.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
/// <summary> /// A sample Upload file manager class. /// </summary> public class UploadFileManager { /// <summary> /// When a file is uploaded to the system. /// </summary> /// <param name = "_fileName">The file uploaded to the system</param> /// <param name = "_fileStream">The file content</param> public void fileUploaded(str _fileName, System.IO.Stream _fileStream) { // creating a single response result to our event call EventHandlerRejectResult eventResult = EventHandlerRejectResult::newSingleResponse(); // check the file for security issues onFileSecurityCheck(_fileName, _fileStream, eventResult); // by default we are storing the file boolean canStoreFile = true; // check if a subscriber set a result of the event call if (eventResult.hasResult()) { // check if the subscriber requested to reject the file canStoreFile == (eventResult.isRejected() == false); } // logic to store the file to database if (canStoreFile == true) { // should be here ;) } } /// <summary> /// Request external file security check. /// </summary> /// <param name = "_fileName">The file uploaded to the system</param> /// <param name = "_fileStream">The file content</param> /// <param name = "_eventResult">The result of file security check</param> delegate void onFileSecurityCheck(str _fileName, System.IO.Stream _fileStream, EventHandlerRejectResult _eventResult) { } } public class MySubscriber { [SubscribesTo(classStr(UploadFileManager), staticDelegateStr(UploadFileManager, onFileSecurityCheck))] public void checkFileSecurity(str _fileName, System.IO.Stream _fileStream, EventHandlerRejectResult _eventResult) { // calling the virus scanner boolean isFileInfected = isFileInfected(_fileStream); boolean isAllowedExtension = false; // get the file extension str fileExtension = System.IO.Path::GetExtension(_fileName); // check for whitelisting if (fileExtension == '.docx') isAllowedExtension = true; // set the event handler result if (isFileInfected || (isAllowedExtension == false)) { _eventResult.reject(); } } } |
Similar logic can be applied to affect the code flow in D365FO in different areas. You can search for all references where the classes EventActionHandler, EventHandlerAcceptResult or EventHandlerRejectResult are used.
We are listing just a few delegates to which you can subscribe and create a custom logic including the Docentric one 😉:
- Docu::delegateScanDocument(),
- Docu::delegateScanDeletedDocument(),
- FileUploadResultBase::delegateScanStream(),
- DocReportTemplate::onUploadingTemplate(),
- and others.
Final Thoughts
Delegates and event handlers can make your life as a developer easier to produce more modular, reusable, easier to maintain code and allow you to interact with others without having to know the implementation parts.
However, be aware of all the pitfalls described in this article especially if you are expecting from the subscriber a result which will change the behavior of your code.