When you access customer or vendor details in Dynamics 365 for Finance and Operations (D365FO), their addresses are listed like this: “456 Peach Road, San Diego, CA 92114, USA”. This is great if you're familiar with San Diego, but if you're in Europe, for example, it might not be very helpful. You might wonder: Is the address in the city center or the outskirts? Normally, you'd have to copy the address and look it up on a map like Google Maps or Bing Maps. But what if you could search for the address directly from D365FO with just the right-click, using the context menu on the address field? This article describes how easily this can be achieved with a minor modification any developer can do in minutes.
The Problem
You can solve this problem with the D365FO standard customization. This involves adding new context menu items by overriding the getContextMenuOptions() and selectedMenuOption() methods for each form control.
This approach works fine if you only need to add a context menu item for a specific element in a specific form. But if you want this new context menu item to be available across the entire D365FO, you'll need to create a lot of extensions. Honestly, this isn't something you can knock out during a lunch break.
The Solution: Docentric Context Menu framework
With Docentric AX version 3.4.7, a new Context Menu framework was introduced. This framework allows you to easily add custom menu options to the D365FO context menus. Unlike the standard method where you need to override methods for each form element, the Docentric framework lets you do it with less hassle.
How Docentric Context Menu framework works
The Docentric framework offers a more streamlined approach compared to the standard D365FO implementation. Here’s a quick breakdown of how it works:
- DocContextMenuManager class:
- Subscribes to the onFormRunCompleted delegate of the FormRun object.
- Calls the static process() method to create a new instance of DocContextMenuManager and starts processing additional context menu options.
- DocContextMenuDefinitionBase class:
- An abstract base class containing all the necessary methods for your custom context menu.
- Simplifies the implementation process for adding custom menu options.
To illustrate, we'll show you how to add a custom context menu option to open an address from D365FO in Google Maps.
Steps to add your Context Menu Option
1. Define your Context Menu class
Create a class that inherits from DocContextMenuDefinitionBase. This is where you define your new context menu option.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
using System.ComponentModel.Composition; // Define the context menu ID for opening the selected address in Google Maps #define.DocOpenGoogleMapsContextMenuId('OpenAddressInGoogleMaps') /// <summary> /// The DocOpenAddressInGoogleMapsContextMenu_POC class defines the context menu /// for opening the selected address in Google Maps for the specified form control. /// </summary> [ExportAttribute(identifierStr(Dynamics.AX.Application.DocContextMenuDefinitionBase)), ExportMetadataAttribute(extendedTypeStr(DocContextMenuDefinitionId), #DocOpenGoogleMapsContextMenuId)] public class DocOpenAddressInGoogleMapsContextMenu_POC extends DocContextMenuDefinitionBase { // Define the menu option ID for opening the address in Google Maps private const int OpenAddressInGoogleMapsOptionId = 1; } |
2. Add Menu Option methods
Implement methods to enable the menu option, add it to the context menu, and specify which form controls it applies to.
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 |
/// <summary> /// Gets the ID of the context menu definition. /// </summary> /// <returns>The context menu definition ID</returns> public DocContextMenuDefinitionId getId() { return #DocOpenGoogleMapsContextMenuId; } /// <summary> /// Indicates whether the custom context menu definition is enabled. /// </summary> /// <returns>True if the context definition is enabled; otherwise false</returns> public boolean isEnabled() { return true; } /// <summary> /// Gets the custom context menu definition for the specified form control. /// </summary> /// <param name = "_formControl">An instance of the FormControl object</param> /// <returns>An instance of the ContextMenu object</returns> public ContextMenu getContextMenuOptions(FormControl _formControl) { ContextMenu menu = new ContextMenu(); // Add the "Open address in Google Maps" menu option to the context menu this.addContextMenuOption(menu, _formControl, 'Open address in Google Maps', OpenAddressInGoogleMapsOptionId); return menu; } /// <summary> /// Checks whether the specified menu option can be added to the context menu for /// the specified form control. /// </summary> /// <param name = "_formControl">An instance of the FormControl object</param> /// <param name = "_menuOptionId">Menu option ID</param> /// <returns>True if the specified menu option can be added to the context menu; otherwise false</returns> public boolean canAddContextMenuOption(FormControl _formControl, int _menuOptionId) { switch (_menuOptionId) { case OpenAddressInGoogleMapsOptionId: // Check whether the "Open address in Google Maps" menu option can be added to the context menu return this.canAddOpenGoogleMapsMenuItem(_formControl); } return false; } /// <summary> /// Checks whether the specified menu option can be added to the context menu /// for the specified form control. /// </summary> /// <param name = "_formControl">An instance of the FormControl object</param> /// <returns>True if the menu item can be added to the context menu; otherwise false</returns> private boolean canAddOpenGoogleMapsMenuItem(Object _formControl) { // Get the field binding for the form control FieldBinding fieldBinding = _formControl.fieldBinding(); if (fieldBinding != null) { // Get the field object for the current field binding DictField dictField = new DictField(fieldBinding.tableId(), fieldBinding.fieldId()); if (dictField != null && dictField.typeId() != 0) { DictType dictType = new DictType(dictField.typeId()); // Check whether the field type of current control is LogisticsAddressing EDT. if (dictType.name() == extendedTypeStr(LogisticsAddressing)) { return true; } } } return false; } |
3. Define Menu Option action
Implement the selectedMenuOption() method to specify what happens when the context menu option is clicked.
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 |
/// <summary> /// Performs an action specified for the selected menu option from the context menu definition. /// </summary> /// <param name = "_selectedOption">Selected menu option from the context menu</param> /// <param name = "_formControl">An instance of the FormControl object</param> public void selectedMenuOption(int _selectedOption, FormControl _formControl) { switch (_selectedOption) { // Open address in Google Maps case OpenAddressInGoogleMapsOptionId: FormStringControl addressControl = _formControl; str address = addressControl.text(); str googleMapUrl = 'https://www.google.com/maps/place/%1/'; const str comma = ','; const str newLine = '\n'; if (address) { // Replace new line characters with comma address = strReplace(address, newLine, comma); // Encode the address string address = System.Web.HttpUtility::UrlEncode(address); // Format the Google Maps URL with the encoded address googleMapUrl = strFmt(googleMapUrl, address); Browser browser = new Browser(); // Open the address in Google Maps in a new browser tab browser.navigate(googleMapUrl, true); } break; } } |
The Result
Here's the complete class you can copy to your environment and try it out.
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 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 |
using System.ComponentModel.Composition; // Define the context menu ID for opening the selected address in Google Maps #define.DocOpenGoogleMapsContextMenuId('OpenAddressInGoogleMaps') /// <summary> /// The DocOpenAddressInGoogleMapsContextMenu_POC class defines the context menu /// for opening the selected address in Google Maps for the specified form control. /// </summary> [ExportAttribute(identifierStr(Dynamics.AX.Application.DocContextMenuDefinitionBase)), ExportMetadataAttribute(extendedTypeStr(DocContextMenuDefinitionId), #DocOpenGoogleMapsContextMenuId)] public class DocOpenAddressInGoogleMapsContextMenu_POC extends DocContextMenuDefinitionBase { // Define the menu option ID for opening the address in Google Maps private const int OpenAddressInGoogleMapsOptionId = 1; /// <summary> /// Gets the ID of the context menu definition. /// </summary> /// <returns>The context menu definition ID</returns> public DocContextMenuDefinitionId getId() { return #DocOpenGoogleMapsContextMenuId; } /// <summary> /// Indicates whether the custom context menu definition is enabled. /// </summary> /// <returns>True if the context definition is enabled; otherwise false</returns> public boolean isEnabled() { return true; } /// <summary> /// Gets the custom context menu definition for the specified form control. /// </summary> /// <param name = "_formControl">An instance of the FormControl object</param> /// <returns>An instance of the ContextMenu object</returns> public ContextMenu getContextMenuOptions(FormControl _formControl) { ContextMenu menu = new ContextMenu(); // Add the "Open address in Google Maps" menu option to the context menu this.addContextMenuOption(menu, _formControl, 'Open address in Google Maps', OpenAddressInGoogleMapsOptionId); return menu; } /// <summary> /// Performs an action specified for the selected menu option from the context menu definition. /// </summary> /// <param name = "_selectedOption">Selected menu option from the context menu</param> /// <param name = "_formControl">An instance of the FormControl object</param> public void selectedMenuOption(int _selectedOption, FormControl _formControl) { switch (_selectedOption) { // Open address in Google Maps case OpenAddressInGoogleMapsOptionId: FormStringControl addressControl = _formControl; str address = addressControl.text(); str googleMapUrl = 'https://www.google.com/maps/place/%1/'; const str comma = ','; const str newLine = '\n'; if (address) { // Replace new line characters with comma address = strReplace(address, newLine, comma); // Encode the address string address = System.Web.HttpUtility::UrlEncode(address); // Format the Google Maps URL with the encoded address googleMapUrl = strFmt(googleMapUrl, address); Browser browser = new Browser(); // Open the address in Google Maps in a new browser tab browser.navigate(googleMapUrl, true); } break; } } /// <summary> /// Checks whether the specified menu option can be added to the context menu for /// the specified form control. /// </summary> /// <param name = "_formControl">An instance of the FormControl object</param> /// <param name = "_menuOptionId">Menu option ID</param> /// <returns>True if the specified menu option can be added to the context menu; otherwise false</returns> public boolean canAddContextMenuOption(FormControl _formControl, int _menuOptionId) { switch (_menuOptionId) { case OpenAddressInGoogleMapsOptionId: // Check whether the "Open address in Google Maps" menu option can be added to the context menu return this.canAddOpenGoogleMapsMenuItem(_formControl); } return false; } /// <summary> /// Checks whether the specified menu option can be added to the context menu /// for the specified form control. /// </summary> /// <param name = "_formControl">An instance of the FormControl object</param> /// <returns>True if the menu item can be added to the context menu; otherwise false</returns> private boolean canAddOpenGoogleMapsMenuItem(Object _formControl) { // Get the field binding for the form control FieldBinding fieldBinding = _formControl.fieldBinding(); if (fieldBinding != null) { // Get the field object for the current field binding DictField dictField = new DictField(fieldBinding.tableId(), fieldBinding.fieldId()); if (dictField != null && dictField.typeId() != 0) { DictType dictType = new DictType(dictField.typeId()); // Check whether the field type of current control is LogisticsAddressing EDT. if (dictType.name() == extendedTypeStr(LogisticsAddressing)) { return true; } } } return false; } } |
Final Thoughts
Developers can easily add context menus to all form controls in D365FO using the Docentric AX framework. The simplest way is to find a type to which you want to apply the context menu.
Keep in mind, the framework isn't all-powerful 😜, so there are a few limitations worth mentioning, which you normally won't notice:
- If a custom context menu option is already added to a form control at design time, your custom context menu option added through the Docentric AX framework won't be visible.
- Custom context menu options can't be added to form controls for which a context menu is already defined using registerOverrideMethod() in a form extension.
- Controls added at runtime also can't have custom context menu items.