D365 Map

Share this:

Each item in a D365 map stores a key and a corresponding value, making a pair. Each key in a map collection is unique, but the associated value does not need to be. Additionally, the data type of the key and the value does not need to be the same. For example, a developer can store a string SalesId in the key, and the sales order total real amount in the value. Or, the developer can store an Int64 RecId pointing to a record in a key, and the RecId of a related record in the value. Maps are similar to a Dictionary in C#.

Types of Collection Classes

Before going into the details of a D365 map, remember that the map class is one of the collection classes.

As an overview, there are five different types of D365 collection classes defined in the base Microsoft code. To explain, these special types of classes provide additional functionality that helps developers. They help store, find, and iterate through lists of objects. They each work a little bit differently.

  • Array – The Array class must hold values of the same data type. They are often used to hold multiple objects or records.
  • List – Used to store sequential values, which are all of the same data type. There are methods to make it easy to add values at the beginning or the end of the list.
  • Set – Every value added to a set must be of the same data type. However, values are not stored in the order in which you add them. Instead, they are optimized for checking if the same value already exists in the set. If you try to add a value that already exists, the duplicate value will not be added. This class is great when you are working with a unique set of values.
  • Map – Used to store a unique key and an associated value. Similar to a Dictionary in C#. All keys must have the same data type. Additionally, all values must have the same data type.
  • Struct – Similar to a class, these objects store multiple values relating to a specific entity together. For example, you might store a customer’s name, birthday, and address as a single item. A struct uses string indexes. A container, which is similar, uses numeric indexes.

To learn more, click on the above links for a detailed explanation of each D365 collection class.

Setup

To test the code shown in the sections below, first create a new project in Visual Studio.

Second, create a runnable class (job). To do this, right-click on the project and select Add>New Item. Then select ‘Runnable Class (Job)‘, and give the job a name. I named mine tutorialMap.

Third, copy the code sections below into the ‘main‘ method.

Fourth, right-click on your runnable class (job) and select ‘Set as Startup Object‘. This will enable it to run when you click the ‘Start‘ button.

Finally, after copying the code below, click the ‘Start‘ button in Visual Studio. This will first build the code. Then, if there are no errors, this will open a browser, navigate to the D365 runnable class (job), and run the code. View the info message output to see the results.

Defining A Map

First, when a D365 map is created, specify the data type of the keys and values that will be stored. This cannot be changed.

    // Create a map that stores a key that is of type String, and a value that is of data type Real.
    Map map = new Map(Types::String, Types::Real); 

Notice, the Types enum is passed into the constructor. These are the various options that the Types enum has.

  • AnyType
  • BLOB
  • Class
  • Container
  • Date
  • Enum
  • Guid
  • Int64
  • Integer
  • RString
  • Real
  • Record
  • String
  • Time
  • UserType
  • UtcDateTime
  • VarArg
  • VarString
  • void

Typically, I always pass in a Types enum value. However, optionally use a function to get the value from an Extended Data Type. See this example.

    // Create a list of whatever data type the SalesId extended data type uses. Hint: It is of type String. 
    Map map = new Map(extendedTypeNum(SalesId), Types::Real);
    
    // The above line would be the same as this for the Extended Data Type 'SalesId'.
    Map map = new Map(extendedTypeNum(SalesId), Types::Real);

Add Values To A Map

Second, after instantiating a D365 map, the next step is to add key-value pairs to the map. Use the ‘insert‘ method to add a key and a value to the end of the map. See this example where a SalesId string and an associated value of type Real are added to the map.

// Create a map that stores a key that is of type String, and a value that is of data type Real.
Map map = new Map(Types::String, Types::Real);

//Add key-value pairs to the map.
map.insert("SO-123", 123.45);
map.insert("SO-789", 789.01);

Duplicate Keys Will Overwrite The Existing Pair’s Value

Remember, the key value must be unique. If the system tries to insert a key into the map when the key already exists, a new pair will NOT be added. Instead, the system will find the existing key-value pair and overwrite the current value with the new value. Consider this example:

// Create a map that stores a key that is of type String, and a value that is of data type Real.
Map map = new Map(Types::String, Types::Real);

//Add key-value pairs to the map.
map.insert("SO-123", 123.45);
map.insert("SO-789", 789.01);

//Attempting to add the same key again will not insert a new pair.
//Instead, it will overwrite the existing value
map.insert("SO-789", 555.55); //555.55 will become the new value associated with key "SO-789"

Conversely, a map can have multiple unique keys that store the same value. See this diagram.

Loop Through A Map

Third, now that there are pairs in the D365 map, loop through and print out the keys and values. In order to loop through a map, we need to use a class named MapEnumerator. See the example below.

// Create a map that stores a key that is of type String, and a value that is of data type Real.
Map map = new Map(Types::String, Types::Real);
MapEnumerator mapEnumerator;

//Add key-value pairs to the map.
map.insert("SO-123", 123.45);
map.insert("SO-789", 789.01);

//Attempting to add the same key again will not insert a new pair.
//Instead, it will overwrite the existing value
map.insert("SO-789", 555.55); //555.55 will become the new value associated with key "SO-789"

mapEnumerator = map.getEnumerator();
while (mapEnumerator.moveNext())
{
    info(strFmt("Key: %1 Value: %2", 
        mapEnumerator.currentKey(), 
        mapEnumerator.currentValue()));
}

Start by declaring a variable of type MapEnumerator. Next, call the method ‘getEnumerator()‘ on the map to get an instantiated map enumerator. Then, set the result to the map enumerator variable.

Then, create a ‘while‘ loop. Inside the while loop, call the method ‘moveNext()‘ on the map enumerator. If there is a next value in the map, it will move to it and return true. If there are no more values in the map, this method will return false. Note that this will cause the inside of the while loop to no longer be executed.

Next, call the method ‘currentKey()‘ to get the key out of the map. Then, call the method ‘currentValue()‘ to get the value out of the map.

Finally, use the ‘info‘ and ‘strfmt()‘ methods to print out the message.

The result should look like this:

Remove Values From Map

Unlike a list, an element can be removed from a D365 map directly without an iterator. Use the ‘delete‘ method to remove pairs from the map. To use this method, pass in the key of the pair you wish to remove.

// Create a map that stores a key that is of type String, and a value that is of data type Real.
Map map = new Map(Types::String, Types::Real);
MapEnumerator mapEnumerator;

//Add key-value pairs to the map.
map.insert("SO-123", 123.45);
map.insert("SO-789", 789.01);

//Attempting to add the same key again will not insert a new pair.
//Instead, it will overwrite the existing value
map.insert("SO-789", 555.55); //555.55 will become the new value associated with key "SO-789"

map.insert("SO-888", 888.01);

//remove the pair with key "SO-123".
map.remove("SO-123");

mapEnumerator = map.getEnumerator();
while (mapEnumerator.moveNext())
{
    info(strFmt("Key: %1 Value: %2", 
        mapEnumerator.currentKey(), 
        mapEnumerator.currentValue()));
}

The result looks like this:

Exists and Lookup

In my opinion, the most useful methods on the D365 map are the ‘exists‘ and ‘lookup‘ methods. The ‘exists’ method is used to check if a key exists in the map. If it does, use the ‘lookup‘ method to find the key-value pair for the specified key.

// Create a map that stores a key that is of type String, and a value that is of data type Real.
Map map = new Map(Types::String, Types::Real);
MapEnumerator mapEnumerator;
str keyToFind = "SO-888";
real value;

//Add key-value pairs to the map.
map.insert("SO-123", 123.45);
map.insert("SO-789", 789.01);
map.insert("SO-888", 888.01);

//Always call 'exists' before calling lookup to prevent a runtime error.
if (map.exists(keyToFind))
{
    value = map.lookup(keyToFind);
    info(strFmt("The key %1 has an associated value of %2", keyToFind, value));
}
else
{
    throw error(strFmt("The key %1 was not found in the map.", keyToFind));
}

The results look like this:

Importantly, if the lookup method is called for a key that does not exist in the map, a runtime error will occur. Therefore, before calling the lookup method, always call the exists method first.

// Create a map that stores a key that is of type String, and a value that is of data type Real.
Map map = new Map(Types::String, Types::Real);
MapEnumerator mapEnumerator;
str keyToFind = "SO-555";
real value;

//Add key-value pairs to the map.
map.insert("SO-123", 123.45);
map.insert("SO-789", 789.01);
map.insert("SO-888", 888.01);

//because "SO-555" does not exist, and we are calling 'lookup' instead of 'exists'
//I expect this to cause a runtime error.
value = map.lookup(keyToFind);

Count of Values

Furthermore, there may be times you need to know the number of values in a D365 set. Use the ‘elements‘ method to return the number of values in the list.

// Create a map that stores a key that is of type String, and a value that is of data type Real.
Map map = new Map(Types::String, Types::Real);

//Add key-value pairs to the map.
map.insert("SO-123", 123.45);
map.insert("SO-789", 789.01);
map.insert("SO-888", 888.01);
    
info(strfmt("There are %1 values in the map.", map.elements()));

Example

For an example of a D365 map in the base Microsoft code, look at the class BomCalcBase. A map is created to store a string key and a Real value. Then, in the ‘addTotalItemConsumption‘ method, the map is populated with an ItemId/InventDimID key, and a corresponding consumptionQty value.

Iterators vs. Enumerators

Interestingly, there are two ways of looping through a D365 set. Use the MapEnumerator class or use the MapIterator class.

As a best practice, you should use the MapEnumerator. It requires less code and performs slightly better.

MapEnumerators cannot be modified while traversing through the values. Whereas a MapIterator can insert or delete elements during traversal. Therefore, there may be rare cases when you need to use a MapIterator. See this example for the different methods involved.

// Create a map that stores a key that is of type String, and a value that is of data type Real.
Map map = new Map(Types::String, Types::Real);
MapIterator mapIterator;

//Add key-value pairs to the map.
map.insert("SO-123", 123.45);
map.insert("SO-789", 789.01);
map.insert("SO-888", 888.01);

mapIterator = new MapIterator(map);
while (mapIterator.more())
{
    info(strFmt("Key: %1 Value: %2",
        mapIterator.key(),
        mapIterator.value()));
    
    mapIterator.next();
}

Notice, the MapIterator uses the method more(), instead of moveNext() in the while loop. Additionally, the more() method only returns whether there are more elements in the set. It does not move to the next value in the list. Therefore, a call to next() is needed inside the loop to move to the next value. This approach requires one extra line of code compared to SetEnumerator.

Pack and Create

Moreover, a D365 map can be packed into a container object and unpacked. This is helpful when working with certain batch jobs that have existing functionality dependent on containers. See this example.

// Create a map that stores a key that is of type String, and a value that is of data type Real.
Map map = new Map(Types::String, Types::Real);
Map newMap;
container c;

//Add key-value pairs to the map.
map.insert("SO-123", 123.45);
map.insert("SO-789", 789.01);
map.insert("SO-888", 888.01);

// Pack the map into a container
c = map.pack();
    
// Create a new map from the packed map
newMap = Map::create(c);

Microsoft Documentation

In order to learn more about how to use a D365 map, check out the Microsoft documentation here. On this page, you can see all of the available helper methods on the class.

I also want to thank Tim Fluharty, whom I am learned a great deal from, and who provided information for this series of articles.

Conclusion

The ability to store a pair of values in a collection is why a D365 map is so useful. Instead of creating a temporary table with just two fields, a developer can use a map to store two related values. Then, add multiple pairs to this map, and later look up specific values quickly using the lookup method. Oftentimes, the key is a simple type such as a string or int65, making it very readable. But then, the value can be complex, such as an entire class object. This is not something a temporary table can do. This makes maps extremely useful when writing code to keep track of two related things.

Peter Ramer
Peter Ramer is a part of the Managed Application Services team at RSM working on Microsoft Dynamics 365. He focuses on the Retail and Commerce industries. When he is not solving problems and finding ways to accelerate his clients' business, he enjoys time with his three kids and amazing wife.

Share this:

Leave a Reply

Your email address will not be published. Required fields are marked *

Proudly powered by WordPress | Theme: Baskerville 2 by Anders Noren.

Up ↑