Reusing Addresses in Dynamics AX 2012: Part 2 – User Interface

By Tory Bjorklund | July 19, 2016

In my previous article on address standardization, I showed how to create a C# class to handle the JSON serialization for a call to a RESTful HTML service. In this article, we will take the next logical step and integrate it into the AX user interface. Some of what I will cover is explained in much more detail in this white paper titled  "Implementing the Global Address Book Framework."

Objective

Our objective in this article is to hook into the Global Address Book (GAB) Framework to make sure that no address can be added to the GAB without first being standardized. We also want to be able to search for existing (standardized) addresses in the GAB to reuse them rather than creating superfluous address records.

Architectural Decisions

One can imagine that there are several different ways to accomplish our task. We need to make a few high-level architectural decisions before we design our solution.

Separation of Concerns

The first decision is where the code should reside. It would be tempting to integrate the code into the forms used to enter addresses. This would serve our purpose most of the time. But what happens when we want to use the DirAddressService to create new addresses? We could end up getting non-standardized records in the GAB from this service.

From an architectural perspective, we want to follow the Separation of Concerns principle. This principle will tell us that it is the concern of the Business Logic to enforce the business rule that all addresses must be standardized. So we need to place our standardization code in the Business Logic layer.

There is a class that provides the business logic to create a new postal address. This class is the LogisticsPostalAddressEntity. Because this class centralizes the logic for creating a postal address, it would make sense to add the business logic here that would return the correct LogisticsPostalAddress record, either a newly created one or a retrieved one when applicable.

Violations of the Separation of Concerns

Having worked through the architectural theory of where to put the code, we now are faced with the reality that Microsoft has violated the principle of the Separation of Concerns. The form that is used to enter new addresses (LogisticsPostalAddress) contains all kinds of business logic that probably should not be contained in the presentation layer. The form also does not use the  LogisticsPostalAddressEntity class that is recommended for use by Microsoft.

So we need to look further for a centralized place to standardize addresses. We are in luck because the LogisticsPostalAddressEntity class  uses LogisticsPostalAddressMap. If we place our standardization in the map we can standardize the address fields of any tables that use the map, including the LogisticsPostalAddress table.  A map abstracts one or more tables into a single wrapper that allows common code to be used to referenced by each table using the map. See this article for more information.

Here is the plan. We will tie into the modifiedField() method. When any of the address components get modified we will call a standardization method on the map to standardize the address. In the standardization method we will check to see if the address is complete enough to standardize. If so, we can standardize the address.

First, we will add the following new method and code to the LogisticsPostalAddressMap:

public void standardizeAddress()
{
    SmartyStreetLib.SmartyStreetAPI api;
    SmartyStreetLib.Address[] addresses;
    SmartyStreetLib.Address address;
    SmartyStreetLib.AddressComponents components;
    SmartyStreetLib.AddressMetadata metaData;
    System.Exception netExcepn;
    int addressCount;
    str output;
    str recordType;
    System.Decimal decimal;
 
 
    // First verify the address is a US address
    if(this.CountryRegionId != "USA")
    {
        // If it isn't blank it isn't USA
        if(this.CountryRegionId != "")
        {
            info("LogisticsPostalAddressView::standardizeAddress() only supports USA addresses");
            return;
        }
        // since it is blank let's assume it's USA
        else this.CountryRegionId = "USA";
    }
 
    // Now verify that we have enough info to standardize
    if(this.Street =="" || ((this.City == "" || this.State == "") && this.ZipCode == ""))
    {
        info("Must have a Zip Code or City and State to standardize an address.");
        return;
    }
 
    
    // Now standardize the US addres
    api = new SmartyStreetLib.SmartyStreetAPI();
    api.set_AuthID(SmartyStreetHelper::getAuthID());
    api.set_AuthToken(SmartyStreetHelper::getAuthToken());
    api.set_Candidates(1);
 
    api.set_Street(this.Street);
    api.set_City(this.City);
    api.set_State(this.State);
    api.set_ZipCode(this.ZipCode);
 
 
 
    try
    {
        addresses = api.DownloadSerializedJsonData(api.BuildUrlQuery());
        // addresses = new SmartyStreetLib.Address[1]();
        address = addresses.GetValue(0);
        components = address.get_components();
        metaData = address.get_metadata();
        this.County = metaData.get_county_name();
        recordType = metaData.get_record_type();
        if(recordType == "P")
        {
            this.PostBox = components.get_primary_number();
        }
        // building compliment
        this.City = components.get_city_name();
        decimal = new System.Decimal(metaData.get_latitude());
        // this.Latitude = decimal;
        decimal = metaData.get_longitude();
        // this.Longitude = decimal;
        this.State = components.get_state_abbreviation();
        this.Street = address.get_delivery_line_1();
        this.StreetNumber = components.get_primary_number();
        this.ZipCode = components.get_zipcode();
        // this.County = metaData.get_county_name();
        // this.DistrictName = metaData.get_dst();
 
 
    }
    catch (Exception::Error)
    {
        info("Caught 'Exception::Error' in LogisticsPostalAddressView.standardizeAddress()." );
    }
    catch (Exception::CLRError)
    {
        info("Caught 'Exception::CLRError' in LogisticsPostalAddressView.standardizeAddress().");
        netExcepn = CLRInterop::getLastException();
        info(netExcepn.ToString());
    }
    catch
    {
        info("Caught an Exception in LogisticsPostalAddressView.standardizeAddress().");
    }
}

Now we will hook into the modifiedField() method to call our standardizeAddress() method. If you look at the modifiedField() method on the LogisticsPostalAddressMap you will see a series of Case statements. We want to standardize any time the Street, City, State or ZipCode change. So we need to find the relevant case statements and add a call to the standardizeAddress() method. For example, find the code for the City field case statement ( case extendedTypeNum(LogisticsAddressCity) ) and then insert the following line just before the call to formatAddress()

this.LogisticsPostalAddressMap::standardizeAddress();

Add this call for the Street, State and ZipCode case statements also.

Now test out your code. Any time you have enough data to standardize an address the Smarty Streets API will be called. For example, if I add or edit an address that looks like this:

Tory B_Edit Address

When I tab to the next field it now looks like this:

Tory B_Edit Address 2

Notice that the street field has now been standardized.

It is important here to mention that I haven’t actually reused any addresses. I have simply ensured that all addresses that are added to the system are correctly standardized. To reuse them we would need to implement a methodology to use the standardized address to search for existing addresses. I will address this issue in a future post.

In my next post, I will attempt to implement the same solution using AX code without a C# assembly. That should be interesting so stay tuned.


Under the terms of this license, you are authorized to share and redistribute the content across various mediums, subject to adherence to the specified conditions: you must provide proper attribution to Stoneridge as the original creator in a manner that does not imply their endorsement of your use, the material is to be utilized solely for non-commercial purposes, and alterations, modifications, or derivative works based on the original material are strictly prohibited.

Responsibility rests with the licensee to ensure that their use of the material does not violate any other rights.

Start the Conversation

It’s our mission to help clients win. We’d love to talk to you about the right business solutions to help you achieve your goals.

Subscribe To Our Blog

Sign up to get periodic updates on the latest posts.

Thank you for subscribing!