Code highlighting

Showing posts with label Extensions. Show all posts
Showing posts with label Extensions. Show all posts

Monday, September 04, 2017

Development tutorial: insert_recordset using the Query class

Introduction

I am sure most of you are familiar with set-based X++ CUD operators: insert_recordset, update_recordset and delete_from. They allow performing database operations with a large number of records in a single roundtrip to the server, instead of a row-by-row type of operation, which depends on the number of rows being processed. As a result, they can provide a very significant boost in overall performance of a selected flow.

If not familiar or just need a refresher, you can read more about it by following the link to MSDN.

Problem statement

There's however one significant drawback with these operators - they are all compile-time constructs, so they lack the flexibility of the flow modifying the database request before it is set based on runtime state of the flow.

And, once the application is sealed, that will mean there is no way to modify the request even at compile-time, as X++ statements are not extensible.

Solution

In Microsoft Dynamics AX 2012 R3 a static method was added on the Query class, that allows to solve the two problems above for insert_recordset. This is of course now also available in Microsoft Dynamics 365 for Finance and Operations: Enterprise edition.

Note: update_recordset and delete_from are still not supported through the Query class.

Example

Imagine that we need to write a function that would copy selected sales order line information into a sales order line history table for one or more orders.

1. Data model

Here's how the data model for this table is defined in this example:

DEV_SalesLineHistory data model diagram
DEV_SalesLineHistory data model diagram

2. New DEV_SalesLineHistory table in Visual Studio

And here's how the new table looks in Visual Studio (I created a new model for it in a separate package dependent on Application Suite):

DEV_SalesLineHistory table in Visual Studio
DEV_SalesLineHistory table
Note I skipped all stuff non-essential to this example

3. Copy function through regular X++ insert_recordset statement

Let's first write the statement for inserting the history records using the regular insert_recordset operator:

public class DEV_Tutorial_InsertRecordset
{
    public static Counter insertXppInsert_Recordset(SalesId _salesId)
    {
        DEV_SalesLineHistory    salesLineHistory;
        SalesLine               salesLine;
        InventDim               inventDim;

        JournalPostedDateTime   postedDateTime = DateTimeUtil::utcNow();
        JournalPostedUserId     postedBy = curUserId();
        SalesDocumentStatus     postingType = DocumentStatus::PackingSlip;

        insert_recordset salesLineHistory
        (
            SalesId,
            LineNum,
            InventTransId,
            SalesQty,
            SalesUnit,
            InventSiteId,
            InventLocationId,
            PostedDateTime,
            PostedBy,
            PostingType
        )
        select SalesId, LineNum, InventTransId, SalesQty, SalesUnit from salesLine
            where salesLine.SalesId == _salesId
            join InventSiteId, InventLocationId, postedDateTime, postedBy, postingType from inventDim
                where inventDim.InventDimId == salesLine.InventDimId;

        return any2int(salesLineHistory.RowCount());
    }
}

As you can see, we do a simple select from SalesLine, specifying the exact fields, joined to selected fields from InventDim, where the field list also contains a few local variables to populate into the rows being inserted.
This is the standard syntax used with X++ insert_recordset statement, which all of you are familiar with.

4. Method signature for Query::insert_recordset()

Now let's convert the above to a Query, and call Query::insert_recordset() instead.
This method accepts three arguments:
  • An instance of a table record. This is where data will be inserted into. We can then use this variable to ask how many rows were inserted, for example.
  • An instance of a Map(Types::String, Types::Container), which defines the mapping of the fields to copy. In X++ operator, this had to be based on the specific order in the field selection lists in the select statement. 
    • The map key is the target field name.
    • The value is a container, which defines a pair of values:
      • the unique identifier of the QueryBuildDataSource object points to the table to copy the value from
      • the field name on the above data source to copy the value from
  • An instance of a Query class, which defines the select statement for the data, similar to what you see in the X++ version above.

As you can see from the above, it does not account for literals, as we did with variables in the X++ operator example.
That is currently not supported with this API.
We can however solve this through a use of a "temporary" table, as suggested below.

5. Define a new table to store calculable literals

Let us define a new table that will store the data required by our insert statement. That means it needs to contain four fields:
  • PostedDateTime
  • PostedBy
  • PostingType
  • SalesId - we'll use this to join to SalesLine. This could be sessionId or whatever is required to ensure concurrency and uniqueness
Here's how the table would look in Visual Studio designer:

DEV_SalesLineHistoryPostingDataTmp table definition

We can now populate this table with the required values and join it to our query.
After executing the bulk insert we can then delete the inserted row (if necessary).


Another possible implementation here could be to use a View, with computed  columns for the different literal values needed. You could select from a table that has only 1 row, like InventParameters or the like. This is however less flexible, as it'll be compiled in, while with a "temporary" table you could determine the values at runtime.

6. Write up the code using the Query::insert_recordset() method

Now we are all set to write the necessary code. It would look like below:

public class DEV_Tutorial_InsertRecordset
{
    public static Counter insertQueryInsert_Recordset(SalesId _salesId)
    {
        DEV_SalesLineHistory    salesLineHistory;
        
        Query query = new Query();
        QueryBuildDataSource qbdsSalesLine = query.addDataSource(tableNum(SalesLine));
        qbdsSalesLine.addSelectionField(fieldNum(SalesLine, SalesId));
        qbdsSalesLine.addSelectionField(fieldNum(SalesLine, LineNum));
        qbdsSalesLine.addSelectionField(fieldNum(SalesLine, InventTransId));
        qbdsSalesLine.addSelectionField(fieldNum(SalesLine, SalesQty));
        qbdsSalesLine.addSelectionField(fieldNum(SalesLine, SalesUnit));
        qbdsSalesLine.addRange(fieldNum(SalesLine, SalesId)).value(queryValue(_salesId));
        QueryBuildDataSource qbdsInventDim = qbdsSalesLine.addDataSource(tableNum(InventDim));
        qbdsInventDim.addSelectionField(fieldNum(InventDim, InventLocationId));
        qbdsInventDim.addSelectionField(fieldNum(InventDim, InventSiteId));
        qbdsInventDim.relations(true);
        QueryBuildDataSource qbdsPostingData = qbdsInventDim.addDataSource(tableNum(DEV_SalesLineHistoryPostingDataTmp));
        qbdsPostingData.addLink(fieldNum(SalesLine, SalesId), fieldNum(DEV_SalesLineHistoryPostingDataTmp, SalesId), qbdsSalesLine.name());
        qbdsPostingData.addSelectionField(fieldNum(DEV_SalesLineHistoryPostingDataTmp, PostedDateTime));
        qbdsPostingData.addSelectionField(fieldNum(DEV_SalesLineHistoryPostingDataTmp, PostedBy));
        qbdsPostingData.addSelectionField(fieldNum(DEV_SalesLineHistoryPostingDataTmp, PostingType));

        Map targetToSourceMap = new Map(Types::String, Types::Container);
        targetToSourceMap.insert(fieldStr(DEV_SalesLineHistory, SalesId),           [qbdsSalesLine.uniqueId(), fieldStr(SalesLine, SalesId)]);
        targetToSourceMap.insert(fieldStr(DEV_SalesLineHistory, LineNum),           [qbdsSalesLine.uniqueId(), fieldStr(SalesLine, LineNum)]);
        targetToSourceMap.insert(fieldStr(DEV_SalesLineHistory, InventTransId),     [qbdsSalesLine.uniqueId(), fieldStr(SalesLine, InventTransId)]);
        targetToSourceMap.insert(fieldStr(DEV_SalesLineHistory, SalesQty),          [qbdsSalesLine.uniqueId(), fieldStr(SalesLine, SalesQty)]);
        targetToSourceMap.insert(fieldStr(DEV_SalesLineHistory, SalesUnit),         [qbdsSalesLine.uniqueId(), fieldStr(SalesLine, SalesUnit)]);
        targetToSourceMap.insert(fieldStr(DEV_SalesLineHistory, InventLocationId),  [qbdsInventDim.uniqueId(), fieldStr(InventDim, InventLocationId)]);
        targetToSourceMap.insert(fieldStr(DEV_SalesLineHistory, InventSiteId),      [qbdsInventDim.uniqueId(), fieldStr(InventDim, InventSiteId)]);
        targetToSourceMap.insert(fieldStr(DEV_SalesLineHistory, PostedDateTime),    [qbdsPostingData.uniqueId(), fieldStr(DEV_SalesLineHistoryPostingDataTmp, PostedDateTime)]);
        targetToSourceMap.insert(fieldStr(DEV_SalesLineHistory, PostedBy),          [qbdsPostingData.uniqueId(), fieldStr(DEV_SalesLineHistoryPostingDataTmp, PostedBy)]);
        targetToSourceMap.insert(fieldStr(DEV_SalesLineHistory, PostingType),       [qbdsPostingData.uniqueId(), fieldStr(DEV_SalesLineHistoryPostingDataTmp, PostingType)]);

        ttsbegin;
        
        DEV_SalesLineHistoryPostingDataTmp postingData;
        postingData.PostedDateTime = DateTimeUtil::utcNow();
        postingData.PostedBy = curUserId();
        postingData.PostingType = DocumentStatus::Invoice;
        postingData.SalesId = _salesId;
        postingData.insert();
        
        Query::insert_recordset(salesLineHistory, targetToSourceMap, query);

        delete_from postingData 
            where postingData.SalesId == _salesId;

        ttscommit;

        return any2int(salesLineHistory.RowCount());
    }
}

As you can see, the first part of the code builds a query using the QueryBuild* class hierarchy. The query is identical to the above select statement, with the addition of another join to our "tmp" table to retrieve the literals.
The second part populates the target to source Map object, which maps the fields to insert into to their source.
The third part actually invokes the operation, making sure we have the record populated in our "tmp" table beforehand.

Note. Because the first argument is the record we insert into, we can use it to get the RowCount(), telling us how many records have been inserted.

7. Extensibility aspect

Leaving the code just like it is does not actually make it extensible, as the partners would not be able to add additional fields to copy, or additional conditions/joins to the query. To accomplish this, we'd need to break the logic out into smaller methods with specific responsibilities, or add inline delegates to do the same. Generally speaking, you should always favor breaking the code down into simpler methods over delegates.
I've not done this in the example, as that's not the purpose, but you should follow these guidelines in production code.

8. Execute and test the code

We can execute these methods now, but first we need to ensure we have a test subject, aka a Sales order to practice with. Here's the one I used in the example:

A sales order with 3 order lines
And here's the Runnable Class that invokes the two methods above:

class DEV_Tutorial_InsertRecordset_Runner
{        
    public static void main(Args _args)
    {        
        const SalesId SalesId = '000810';

        if (SalesTable::exist(SalesId))
        {
            Counter xppInsert = DEV_Tutorial_InsertRecordset::insertXppInsert_Recordset(SalesId);
            Counter queryInsert = DEV_Tutorial_InsertRecordset::insertQueryInsert_Recordset(SalesId);

            strFmt('Tutorial Insert_Recordset');
            info(strFmt('Inserted using X++ insert_recordset = %1', xppInsert));
            info(strFmt('Inserted using Query::insert_recordset() = %1', queryInsert));
        }
    }

}

You can download the full project here.


Hope this helps! Let me know if you have any comments.

Thursday, August 10, 2017

Announcement: Plan of record design for InventDim extensibility

As you all hopefully know by now, we are on a journey towards overlayering-free solutions.
One big roadblock on this path was the Inventory dimension concept we have in AX, or, more specifically, providing a way for partners to add new inventory dimensions that would not require overlayering.

Michael has documented the current design we have in mind in a blog post you can read below:
https://blogs.msdn.microsoft.com/mfp/2017/08/10/extensible-inventory-dimensions/

Let me know if you have any questions!

Thursday, March 02, 2017

Extensible enums: Breaking change for .NET libraries that you need to be aware of

A while back I wrote a blog post describing the Extensible Enums - a new feature that is part of Dynamics 365 for Operations:
http://kashperuk.blogspot.com/2016/09/development-tutorial-extensible-base.html

I explained that when marking an enumeration as extensible, the representation of this enum under the hood (in CLR) changes. Here's the specific quote:
The extensible enums are represented in CLR as both an Enum and a Class, where each enum value is represented as a static readonlyfield. So accessing a specific value from the above enum, say, NumberSeqModule::Invent would under the hood look something like NumberSeqModule_Values.Invent, where Invent is of type NumberSeqModule which is an Enum. It would in turn call into AX to convert the specific named constant "Invent" to its integer enumeration value through a built-in function like the symbol2Value on DictEnum.
Something that was not super clear in the post is that this was actually a breaking change that might impact your .NET solutions relying on one of these base enumerations.

Problem statement

As part of enabling further extensibility in the application for the next release, we have made a number of additional base enums extensible.

Let's take enum TMSFeeType as an example. Assume we have made it extensible in X++. That means that in our C# project, where we use this enum, we will no longer be able to access it from Dynamics.AX.Application namespace by name, like so:

switch (accessorialFeeType)
{
    case TMSFeeType.Flat:
        // Do something
        break;

    case TMSFeeType.PerUOM:
        // Do something else
        break;
    // etc.
}

If you navigate to its definition, you will notice that the enum declaration is empty:

namespace Dynamics.AX.Application
{
    public enum TMSFeeType
    {
    }
}

The proper way to use the enum that is extensible is to reference the above mentioned class suffixed with _Values, which lives in the Dynamics.AX.Application.ExtensibleEnumValues namespace, like so:

if (accessorialFeeType == TMSFeeType_Values.Flat)
{
    // Do something
}
else if (accessorialFeeType == TMSFeeType_Values.PerUOM)
{
    // Do something else
}

Note: Because the values are now determined at runtime by going to the AOS and asking for the correct integer value of this enum, they cannot be used in a switch/case block, which expects constant expressions known at compile-time.

What's next

Obviously, this situation is not great. 
Let's hope that Microsoft will think of a good way to address this going forward.

Question to you

That leads to a question - how many of you actually have .NET libraries relying on application code in Dynamics 365 for Operations and might be impacted by us making some of the enums extensible in the next major release?

Sunday, October 02, 2016

Tutorial: Extending the label printing functionality in Microsoft Dynamics AX 7

Today I would like to shed a bit more light onto how to extend the label printing functionality, specifically, show how to add a new field from the work line to be displayed on the label.

This is something we have created as a demo for Tech Conf a while back, and you can still see the recording where Per and Zach from our team showcases the different label printing scenarios.
You can view the different Microsoft conference videos here.

Just as a refresher, here is a guide for how to set up label printing in Dynamics AX 7:
http://kashperuk.blogspot.dk/2016/10/tutorial-label-printing-in-microsoft.html

Technical introduction

The label printing "framework" consists of three main components:

  • WHSLicensePlateLabel table, which contains the information for the label, which is substituted into the label through the variables.
  • WHSLicensePlateLabelBuild class, which is responsible for populating the WHSLicensePlateLabel table with data for a particular work order (line).
  • WHSDocumentRouting class, which is responsible for the actual printing of the label, as well as the substitution of variable values, using the below methods:
    • initMenuFields() method is responsible for building the list of substitute fields based on the WHSLicensePlateLabel table fields
    • printDocument() method finds the specific document routing record that matches the flow criteria (e.g., warehouse, work order type, carrier, etc.), performs the translation of the label and sends it to the selected printer (Note that the label can be printed to multiple printers and you can for each printer decide, which layout to use).
    • translate() method is responsible for actually replacing any variables in the label layout with the corresponding values from the WHSLicensePlateLabel table
Let's take a closer look at the fields available on the WHSLicensePlateLabel table:

WHSLicensePlateLabel table fields
These fields represent the full list of potential variables that can be put into the label in the Document Routing layout. Depending on the item flow, some of these fields are not populated. Note that when setting up the placeholders in the Document Routing Layout form, only visible fields are displayed. That allows for storing some "plumbing" information for each record, like the PrinterSettings.

Adding placeholder variables to a label layout

Fragile Example extension

For some scenarios you might want to display some additional fields on the label, which are not part of the list shown above.

Imagine a scenario where we want to display a "Fragile" message on the label if the items on the work line are accordingly marked. 

Since there is no such field in the label, we will need to extend this functionality. And because we are working in AX 7, we will try to use Extension instead of Overlayering the elements, as we would do in AX 2012 R3.

Step 0 - Create a new model for our changes

First of all, let's create a new model, which will contain all the elements we add through extension.

Create a new model in Visual Studio
We will put this model into a new package, as we do not intent to overlayer existing AOT elements. We will reference the Application Suite package, so that we can access the elements defined in it, such as the ones mentioned above. 
Note that by default the new package will also reference Application Platform.

Select referenced packages for model FragileExample
Confirm the creation of the new model and create a new project for it. Name it FragileExample.

Step 1 - Extend the WHSLicensePlateLabel table and add a new field FragileCode

Since we want to display an extra field in the label, we need to add it to the WHSLicensePlateLabel table. In order to do that, let's create an extension of this table in the new model:

Adding a new table extension from Application Explorer
You need to have selected the right project in your Solution Explorer before you attempt creating an extension. This is to ensure that the extension is actually created in the right model.


We are now going to add a new field of base type String, and call it FragileCode with the corresponding label.

Note I am not going to bother with labels or Extended Data types for sake of simplifying the demo

Table extension for WHSLicensePlateLabel in FragileExample model
New field FragileCode on table extension WHSLicensePlateLabel.Extension
In order for the field to appear in the Document routing layouts form all we need to do is compile the new code and synchronize it against the database. I have decided to do that at build time by configuring it accordingly, as shown below:

Synchronize on build = true for FragileExample project
If we now reopen the Document routing layouts form, we can see that our new placeholder is available in the list and we can add it to the label:

Fragile code placeholder available in Document routing layouts form

Step 2 - Populating the Fragile code field on WHSLicensePlateLabel record

Now that we have the field available on the label, we need to populate it. 
As described in the Technical information section above, the label details are populated in the class WHSLicensePlateLabelBuild. Let's quickly examine how this happens:

Sequence of calls to build a license plate label
As you all know, any and all work creation and execution in Dynamics AX happens through the WHSWorkExecuteDisplay* classes, e.g., purchase order registration can execute WHSWorkExecuteDisplayPOItemReceiving, while general work execution flows can execute WHSWorkExecuteDisplayUserDirected, etc.
Within these classes the WHSLicensePlateLabelBuild class is initialized, and the buildLicensePlateLabels() public method is called, when a label needs to be generated and printed.
Depending on the flow, either the insertSingleLabelPrintLine or the insertSingleLabelMenuItem method will be invoked internally, after which the generated label is printed through the printDocument API of the WHSDocumentRouting class, as described above.

Note. insertSingleLabelPrintLine supports the flow where the Print step is part of the work template lines, while insertSingleLabelMenuItem is for the case, where the Print is configured through the mobile device menu item.

So, in order for us to populate the extra new field we need to look into these insert* methods. With the recent AX 2012 R3 changes these methods have been refactored in a way, where most of the logic for populating the WHSLicensePlateLabel table has been pulled out into a new private initLabel() method.

In Dynamics AX 7 this method has been extended (who could have done that? :)) and now has a delegate which is invoked at the end of the method, which means that event handlers can be created for it. With these minor changes, we can now extend this class by subscribing to the event of the initLabel method being invoked, as shown below:

Copy event handler code for labelInitialized delegate
We can now put this generated event handler signature into a new class in our model, as shown below:

Signature of the event handling method for labelInitialized delegate
This is awesome and is one of the new extensibility language features in Dynamics AX 7 - I have subscribed to the event of this method being called without touching the original class at all.
As you can see, instead I have a SubscribesTo attribute that subscribes me to the corresponding event in a static fashion. 

Now all we need to do is actually populate the Fragile code on the label. Let's do that:

Implementation for the event handling method for labelInitialized delegate
As you see above, the code is super simple, all we do is get the Filter code value from the corresponding item, and populate it into the FragileCode field.

Let's now compile everything and give it a go in AX web client.

You can download the VS project file from my OneDrive here.

AX flow steps and result


  1. Modify item A0001, setting Filter Code 4 to a new value called FRAGILE
  2. Create a new purchase order for 10 pcs of A0001 to WH 24
  3. Modify the Mobile device menu item for Purchase receipt to "Print label"
  4. Ensure you have a Document routing record matching WH 24 for Purchase orders
    1. Ensure you have a document routing layout being sent to a label printer
  5. Receive the above purchase order through the WMDP
As a result, you should get a new label created, which will have the Fragile code populated with the value FRAGILE, as below (I've personalized the form to show the new field):

License plate label with the populated Fragile code

Depending on your label layout you would have this message printed on the label as well.

Conclusion


With this blog post I hoped to show you how to extend the existing set of fields available on the license plate labels.

At the same time we took a look at some of the new language constructs, design paradigms and tooling that allows for a much cleaner approach to extending existing functionality, as compared to overlayering, which many are used to.

Consider extending instead of overlayering next time you need to make a change!

Monday, September 26, 2016

Development tutorial: Extensible base enumerations in Microsoft Dynamics AX 7

Introduction

Microsoft Dynamics AX 7 is a game changer when it comes to applying customizations required for running your specific business. Specifically, the development approach changed to focus a lot more on extending the application instead of over-layering elements to customize application flows.

Over the following months I will try to describe some of the examples of where Microsoft has ensured that the application can be extended without modifying existing AOT elements.

If there is a specific topic you would like me to cover, let me know in the comments.

Topic of today

Today I would like to start by talking about Base Enums. Enums in X++ are defined in AOT to represent a list of literals, or named constants, if you will, which then can be used in the code in a convenient way. They are stored as integers in the database.
As all of you I'm sure are already familiar with this concept, I will no go further into this.
If necessary, here's a refresher: https://msdn.microsoft.com/en-us/library/aa881702.aspx

Base enums have caused a lot of grief when writing customizations or when doing code upgrade, as you always needed to be aware of new values Microsoft might add to the list in future updates, some of the ISVs could add to the list, etc. So normally you'd have to remember to add a gap in values when adding your new enum value.
You also needed to be very careful about changing existing values as to not break the existing data in the database.

In AX 7 it is not that easy. Since there is a much more strict separation into models, with Platform and Foundation pieces being clearly isolated from Application components, customizing by overlayering base enums is not as easy, and not possible in some cases.

Example

Let's look at NumberSeqModule base enum as an example. You all are most probably familiar with this enum, since that's where you would need to add your new application module for new solutions, if you want it to be handled through the common Number sequence framework.
In AX 7 this enum is defined in the Application Platform model, meaning that it cannot have any values that are application-specific, as well as that it cannot be over-layered.

That means that the only way to go is to extend the enum.
If you are not familiar with the concept of extensions in Dynamics AX 7, you can read a good description on our wiki page: https://ax.help.dynamics.com/en/wiki/customization-overlayering-and-extensions/

In order to be able to extend a base enum, it needs to be marked as allowing extensions, which is done through the property IsExtensible.

Note that in Application Platform there are a few enums that are extensible, while the majority does not allow it. That is done on purpose, for performance reasons, based on the belief that partners would not need to extend these enums.

If that is not the case, you should let Microsoft know, so they mark the corresponding enum as extensible and release it as a hotfix.

As a result of extending an enum, a new base enum element will be created in the current model, and you will be able to add one or more element values to it.

At runtime AX will collect the values from the base definition and all extensions across all models and present the combined list of values to the user.

Note that you cannot change the properties of the base enum through extension, only add new enum values.

Difference

The major difference of an enum that has extensions vs the legacy enum is in the way how they are represented under the hood.

The extensible enums are represented in CLR as both an Enum and a Class, where each enum value is represented as a static readonly field. So accessing a specific value from the above enum, say, NumberSeqModule::Invent would under the hood look something like NumberSeqModule_Values.Invent, where Invent is of type NumberSeqModule which is an Enum. It would in turn call into AX to convert the specific named constant "Invent" to its integer enumeration value through a built-in function like the symbol2Value on DictEnum.

You'd need to add a using statement to your project, as shown below:
using Dynamics.AX.Application.ExtensibleEnumValues;
This class containing all the values would be built based on all the extensions of the corresponding enum, thus providing a generic way of handling them.

Nothing changes on the database layer, the values are still stored as integers, as today.

X++ impact

  • What this means for your every-day X++ development is that things like comparison of different enum values are out of the question. So you cannot, for example, write
if (workTable.WorkStatus <= WHSWorkStatus::InProgress)
You would now need to be more explicit, listing all the specific values that are applicable through an equality operator. (Like, Open and InProgress)
  •  This also means that you cannot rely on a certain enum value having a specific integer value, since they will be assigned per-deployment depending on all the different extensions for this enum you have deployed. So doing enum comparisons to their integer values is a Big No from now on.
if (workTable.WorkStatus == 0)
Note that both of the above have been a Best Practice for enums for a while now, so your modifications should hopefully not be impacted, if a certain enum is made extensible.
  • For extensible enums there is still an implicit conversion to and from integer type, but a warning will now be shown, to warn the developer that the enum value might be incorrect as a result of the assignment.

Development environment impact

In Visual Studio the impact is visible for any kind of metadata properties where enum values need to be specified. That includes Fixed field and Related Field Fixed when defining relations on tables and data entities, filter values on Query data source ranges, etc.

For example, when defining a Fixed field relation, you would now be able to select the enum value from a dropdown:

Setting value of an extensible enum on a Fixed field relation

Note how the full notation is used instead of what you'd do before with specifying the integer value of the corresponding value.

On query data source ranges you would instead use the short notation by just specifying one or more enum values, comma separated:

Setting value of an enum on a Query range

Conclusion

As you have seen, extensible enums is a way the platform now supports adding additional values to existing base enums without customizing these enums, which really simplifies upgrade going forward.
It however introduces certain limitations on how you use these enums in X++ code and metadata that you need to familiarize yourself with.

Let me know if you have any questions on this

Monday, September 22, 2008

SysMultiTableLoookup - dynamic lookups based on multiple tables

Wow! MSDN for Microsoft Dynamics AX is getting better every day. This is terrific news!! I still remember the days, when all the information was extremely hard to find. It had its own charm though :)
Anyway, this topic is really not about MSDN. It is about lookup forms.

First of all, for the record: I (and the Best Practices document as well) recommend creating custom lookup forms in AOT instead of dynamically coding them in the overridden lookup methods on controls/datasource fields.
But, in reality, this is true only for lookup forms with very large complexity. I won't go into a discussion of why that is the way it is here. :)

Now, back to what I was planning to write about:
In order to build a lookup form from code, developers use the SysTableLookup class.
You can go to MSDN (mentioned above) to read a How-to article on creating a run-time lookup form, as well as take a quick look at the SysTableLookup method descriptions.

SysTableLookup class has evolved over the multiple releases, providing more and more flexibility and control to the application developers.
I would like to publish another extension to this class, SysMultiTableLookup, which I hope will prove useful to members of the AX community.

Short list of features:
- Backward compatible, should cover everything that is present in AX 2009 version of SysTableLookup class
- Allows including multiple tables into lookups with different join types
- Completely based on the Query that you build, no extra parameters (except for the control) are needed to initialize the class
- Allows adding aggregated fields to the lookup
- Displays fields based on Boolean Enum as check boxes
- Allows to specify alternative labels when adding fields to the lookup

New download link, as Axaptapedia seemed to mess up the file

You can download the project from axaptapedia.com.


It has (to some extent) been tested on Axapta 3.0 SP3, AX 4.0 and AX 2009.

Also included in the project is a tutorial form, showing 4 examples of dynamic lookups using the new class. After importing the project, make sure to try out the form, and use it for future reference for code examples and other inspiration.

Tuesday, August 12, 2008

Another useful Editor Script for developers

In AX the principle of upcasting is used quite a lot. Some examples coule be the RunBase hierarchy or the InventMovement class hierarchy. Basically, the base (or super) classes contain the main part of the hierarchy logic, while subclasses only override a couple of methods performing actions specific to this class.

When browsing the AOT, trying to find a specific line of code (debugging without the debugger, basically), you often find yourself in a situation when you lookup a definition of a method from a derived class, and end up in this method of the base class instead (because upcasting was used in the code in question). And every time you need to see the same implementation in the child class, you need to go back to the AOT and open it from there manually.

Well, not any more.
Here is a small editor script, that you can use to navigate to an overridden method of a derived class.

I created 2 versions of the same script, one using xReferences, the second using Dictionary class. The xRef one, generally, works faster once the xRefs for the class hierarchy is updated (happens the first time you use the script). So I am not posting the second script version. If anyone is interested in that version specifically, let me know and I will send you the code.

I made a couple of small changes to the script since my previous post (which I deleted), making it possible to import the script without compilation errors into AX prior to verion 2009. Sorry for that :)

You can copy-paste the code for the script from axaptapedia.com (There is a problem with copying over the code from blogger site directly)

You can read more about Editor Scripts and how to use them here

Enjoy, and get back to me with any comments or suggestions, posting them as comments or e-mailing me directly

Thursday, May 29, 2008

EditorScripts.addIns_OpenInAOT - version 2

A long time ago, I posted an editor script for opening objects selected in the editor in AOT (link to that post)

Yesterday, miklenew from AxForum posted an editor script for AX 3.0 that opens a new AOT window with the object currently selected in the Editor, that works a little differently and also allows to open objects based on variable type. You can view the original post here.

Today I modified this code, extending it a bit and adding support for Dynamics AX 4.0 and Dynamics AX 2009 (For version 3.0 use the code posted on AxForum).

public void addIns_OpenInAOT(Editor e)
{
#AOT
TreeNode treeNode = TreeNode::findNode(e.path());
xRefTmpReferences xRefTmpReferences;
Column nCol = e.columnNo() + 1;
Line nLine = e.currentLineNo() + 1;
;
treeNode.AOTmakeXref(1);
xRefTmpReferences = xRefCreate::makeTmp(infolog.lastxRef());

select firstonly xRefTmpReferences
order by Column desc
where xRefTmpReferences.line == nLine &&
xRefTmpReferences.Column <= nCol;

if (!xRefTmpReferences)
return;

treeNode = TreeNode::findNode(xRefTmpReferences.path());

if (treeNode)
treeNode.AOTnewWindow();
}


As you can see, the code is very simple and utilizes the xReferences.
But the great thing about it is that updating the cross references is not required for this code to work, as it updates them on the fly for this specific AOT node.
I think this is a must have method for each application developer out there. :)

Monday, April 28, 2008

SysFormEnumComboBox - class allowing to show only some of the values for a comboBox

In Microsoft Dynamics AX 2009, a new class has been created, that got my attention recently after yet another question about this sort of thing was asked by a fellow Dynamics AX developer.

Often enough, we need to restrict the user selection from a particular ComboBox. Creating a new BaseEnum specifically for this purpose is a really cumbersome solution, which later might lead to problems of maintenance.

Originally, the SysFormEnumComboBox was designed to be used on forms to provide for this behavior. I modified the class slightly to allow for usage in RunBase Framework classes.
To demonstrate, how this class can be used, I modified the tutorial_RunBaseForm class and form, showing both scenarios: adding the control from the dialog method of the class, as well as adding it manually in the form design and methods.

To use this class, you need to know only one static method:
public static SysFormEnumComboBox newParameters(
FormRun _formRun,
int _comboBoxControlId,
enumId _enumId,
Set _allowedEnumValuesSet,
Form _form = null)

And here is an example of how you would use it:
sysFormEnumComboBox = SysFormEnumComboBox::newParameters(element, 
control::ComboBoxOnForm,
enumnum(InventTransType),
enumSet);

which means that the control ComboBoxOnForm will be bound to BaseEnum InventTransType, containing only values, found in the Set enumSet.

You can download the project for versions 4.0 and 2009 of Microsoft Dynamics AX through the following link:
download

P.S. It is also worth mentioning, that this class does not provide support for grids. It requires a stand-alone control, not bound to a database table field.

Friday, September 14, 2007

DEV_SysTableBrowser version 2.0 is out!

I have been asked by a number of my blog readers to move this tool to Dynamics AX 3.0, so here is the updated version of this tool. Now it works on both DAX 3.0 and 4.0 (tested on 3.0 SP5 KR2 and 4.0 SP1 and SP2).

Also, I added a couple of things I considered useful to this new release:
- The user field select dialog option has now, by default, all the non-system fields selected, so that users don’t get surprised when no fields are selected. I agree – it does look strange :)
- Now it’s possible to select display methods along with table fields in the user field select dialog. This feature was already working with FieldGroups, so I decided it could be a nice add-on to the User Field List option
- I also fixed a couple of old (and new, brought to us by 4.0 SP2) bugs that existed in the tool
- After reading some of the last posts on AxForum, I decided to add the ability of browsing temporary tables into my project as well, so that others may use them if they want :) (this will also include browsing temp tables data shown in forms from Tabax when the next version comes out)
- Now you can also use the browser for debugging purposes, launching it from code (this works for temp tables as well). Just write:

SysTableBrowser::browseTable(table.TableId, table);

where table is a cursor (second parameter can be omitted for non-temp tables). In this case the browser will open and code execution will stop until you close the browser (if not in a transaction). Third parameter controls if code execution should be stopped.

- There are two variables in method new of class SysTableBrowser
saveQueryRun = true; // Enables/Disables queryRun saving when new options are specified
savePosition = true; // Enables/Disables cursor position saving when new options are specified

The options allow to save the user filters and cursor position when changing setup options.
Both operations could result in performance problems on large tables.
If that happens (or you just don't need them), just turn them to false. :)

The download link is: DOWNLOAD

See extended installation instructions and tool description on: HOMEPAGE

What was a little disappointing is that a number of bugs I wrote about earlier were not fixed since SP1. Here are the 2 I mentioned before, again:
- UPDATE_RECORDSET command, when used in the table browser, crashes DAX.
- The left top edge position (the coordinates) of the table browser gets reset when any of the options are changed. I fixed this by specifying 0 instead of -1 for the leftMode and topMode properties of the design. So when you download this tool, the browser will stay in one place, which is nice ;)

Also, 1 new bug I found is for the Russian localization team.
- Changing the view (in the Unmodified version of the browser was causing the full table list to open, prompting the user for a selection of the table). This is caused by a small validation in the SysTableBrowser::main() method. SysSetupFormRun is supposed to be the classId of the calling object. And it is not, because some system wide changes were made to the SysSetupFormRun::construct() method. Anyway, I fixed this small bug as well.

OK. That’s all about the tool and the bugs. I have also been asked by a couple of my readers to write a tutorial on using the SysListPanel class.

That’s exactly what I am going to write about in my next blog entry. After all, there is a new class extending from the SysListPanel class in the DEV_SysTableBrowser project.

Monday, September 03, 2007

DEV_CreateNewProject tool version 2.0.0

Hello, everyone.

I finally got down to moving the latest version of this tool to Dynamics AX 4.0.
Now there is only one project for both versions (3.0 and 4.0)

If you haven't seen this project before, you are welcome to visit the homepage, which contains a description of the features, screenshots, installation instructions and more.

The download link with the new project is:
DEV_CreateNewProject version 2.0.0

Important:
The project was renamed to fit the naming conventions of Dynamics AX. The new name is DEV_CreateNewProject.

(If you want, you can remove the previous version (AxCreateNewProject)).
Leaving them in your application WILL NOT have unwanted effects on your system


Also, I would like to share some knowledge that came upon me today with you.
This might not seem strange to you, but it sure was a surprise for me.
While testing the new version in Dynamics AX 4.0 SP2, I noticed that the form size and position are not saved when I press OK in it.
I dug deeper into system classes and found one extra condition that was added before any data for the form is saved.

The extra condition is:
The AllowUserSetup property on form design must be Restricted or Yes.

So you should keep this in mind when developing forms in the future.

Well, have a great day, all of you.
And do try out the new DEV_CreateNewProject version.

Wednesday, June 27, 2007

Tabax plugins' update - RecentWindows, RecentProjects

After developing 3 great tabax plugins I was feeling a little unsatisfied, because I wanted to add images to the popup menues.

AndyD has developed a class that allows me to do this now.

The project containing the class with the images is "shipped" separately. Depending on where you want to see images in your popups menues or not, you will be able to import it separately if you do.

Download:
You can use the following links to download the projects (version 0.1.1) and the MenuImages class.

RecentProjects v. 0.1.1 -download
RecentWindows v. 0.1.1 - download
Class MenuImages - download

Alternatively you can download them from their homepages on Axaptapedia.

RecentProject - homepage
RecentWindows - homepage

Screenshot: (RecentWindows)

Tuesday, June 26, 2007

AxPaint - make your DAX look cool :)

A couple of months ago I asked Alex Kotov from AxForum to modify a project he wrote for fun half a year ago or so. Today he sent me an e-mail with the project.

A short description of what the tool can do for you: :)
Basically, it just changes the backcolor of your DAX 3.0 (it does not work for DAX 4.0) installation.
You can choose a simple custom color you would like to use, or select an image from your hard drive to use as the background image.
you can select a couple of settings as well (ALL the settings are accessed by pressing Alt + S in DAX after launching the AxPaint form) - like stretching the image or using a 'transparent' background, showing your desktop (you have to set the image path to a specific location for that).

Nothing much, you'd say. But it sure is fun to work in DAX when a nice picture is seen in the background. :)

Installation instructions:
  1. Unzip the archived files.
  2. Place the .bat and .ocx files somewhere where you won't delete them accidently.
  3. Run the .bat file, which will register the .ocx
  4. Import the project into Dynamics AX - the project contains one form AxPaint.
  5. Run the form. It will automatically hide from view. Press Alt + S to run setup.
Custom settings (my preference):
  1. The image path is setup to C:\Documents and Settings\%username%\Local Settings\Application Data\Microsoft\Wallpaper1.bmp - this is the path to the current wallpaper in Win XP (without active desktop)
  2. Show desktop (like Delphi) option is set in the setup dialog.
  3. In the startupPost method of the Info class the following lines are added:  
  4. An external application is used to change the wallpaper on system startup. So every day I have a different look in my DAX :)
Project archive download link:
download

Custom code for the info.startupPost method:


 if (curUserId() == 'ikash')
    TreeNode::findNode("Forms\\AxPaint").AOTrun();


Link to the external WallChanger application:

download

Screenshots:

Monday, June 25, 2007

3 Dialog extensions

Performing one of the modifications on the project I am currently working on, I had to modify the behavior of one of the basic classes using the RunBase framework.

Anyway, what I needed to do was:
  1. Override the basic lookup for a specific control on the dialog.
  2. Prevent user interaction with other forms until the dialog is closed.
  3. Make some of the controls in the dialog mandatory.
Well, the perfect solution here would be to create a Form, and use it as the dialog.
But the code was very highly customized, so it would be a hard job.
So I decided to go another way, using what I have on hand, and that is the Dialog class.
For that, I needed a couple of modifications to the Dialog class, as it does not allow any of the 3 features I neded.

But what any developer should clearly understand when using dialogs is - this is just another form. Simple as that - Dialog is just another AOT form. What this means is that with a dialog we can do all the things we can do with a form.
Also, which is very nice and I thank DAX Developers for that, RunBase framework uses DialogRunBase class for showing forms, and this class is extending from class Dialog, so changes made to Dialog will also be available in RunBase framework.

Here is a small explanation of what has been modified:
  • Added a class Dialog variable showModal, which controls the modal state of the used dialog. 
  • Added a parm method to set the value of the showModal variable. So, basically, to make a modal dialog you simply have to call this method passing true as the parameter.
  • Added a method SetFormModal, which actually does all the enabling/disabling of windows (DAX 3.0 only, as DAX 4.0 already has built-in modal forms)
  • Modified runOnClient() method to allow server dialogs to run modal as well
  • Modified wait() method, which calls SetFormModal method (DAX 3.0 only)
  • AddField and AddFieldd modified to allow specifying a specific name for a control.
  • DialogField class was modified to allow setting the mandatory property (mandatory(), unpack() methods) and specifying a specific name for a control (new and fieldname methods)
The project contains 4 objects:
  • Class\Dialog - only modified methods
  • Class\DialogField - only modified methods
  • Class\Tutorial_RunBaseForm
  • Jobs\tutorial_DialogModalMandatory
The tutorials show the features added to the Dialog and DialogField classes (one of them is present in the basic application and is extending from RunBase, using an AOT form, the second is a job, showing the use of Dialog class)

DAX 3.0 Download link (was created in DAX 3.0 SP5 KR2) - DOWNLOAD
DAX 4.0 Download link (was created in DAX 4.0 SP1 EE) - DOWNLOAD

Important:
After importing the project do not forget to compile forward the Dialog class and to restart the DAX client.

Certain Issues you should know about:
  1. InfoActions, if present, will allow to access forms, if the infolog window with them was opened before the modal dialog was called.
  2. Mandatory property starts working only after a value is entered into the control. What this means is that if you simply press OK right after the dialog is opened, you won't receive an error (regular forms works this way too)
Credits
The modification allowing to specify a specific name for a DialogField was originally posted on the Russian Axapta forum

Tuesday, May 15, 2007

(DAX 3.0) SysExportDialog form extension

After working for some time with DAX 4.0 you can't help but notice the names given to exported xpo files - all the objects all prefixed with the type of the exported object.
I found this to be very convenient, that's why I wanted to lay my hands on the SysExportDialog form and add this feature in there as well.
And that's exactly what I did today.

This turned out to be a task a little harder than I'd hoped, because there are special methods on the TreeNode class that allow to see the type of the object. Those methods are not present in DAX 3.0 yet, so I had to write something similar (probably on a more primitive way, as this is the only place the code will be used - unlike the DAX 40 modifications which are now available everywhere the Treenode Class is used)

Also, I "fixed" a small issue that was present in DAX 4.0 with this: exporting an entire node of objects (e.g, Forms) resulted in the following name of the file : "_Forms.xp".

Anyway, here is the xpo file -- notice the neat name of the file ;). -- download Form_SysExportDialog.xpo
The file contains the SysExportDialog form only.
The original form was taken from DAX 3.0 SP5 KR2.

Hope you like it as much as I do ;)

Saturday, April 07, 2007

Add-On: Extended SysTableBrowser

Hello, readers.
Today I provide you with another tool for Dynamics AX developers. It is a modified version of the SysTableBrowser (See picture 1).
picture 1


Important:
After importing the project refresh the AOD (Tools\Development Tools\Application Objects\Refresh AOD)

Modifications:

  1. The table browser does not change its position when a different setup is selected. (Unlike standard system behavior in DAX 4.0)

  2. The AutoReport option was extended, and now you are able to select any of the table field groups (See Credits).

  3. Another selection option was added, providing the means to select any of the table fields to be shown in the grid. To do this, just press the ‘Select Fields’ button and select the fields you want displayed (See picture 2).

  4. Added a list of presets for the SQL window. (SELECT, UPDATE, DELETE). The list can be easily extended to include other presets you often use.

  5. Extra logic was added to the ExecuteSQL button, which executes the SQL string that the user has put into the SQL window. If it is a simple select (for example, generated by the SELECT preset), the ds query is executed instead of the sql string. This will bring back the sorting and filtering options that disappear after using the sql string query.
  6. Added the ability to sort columns by name or by id. In some cases (when, for example, you cannot find a specific field) it really helps out! (See picture 3)

picture 2
picture 3


Possible future improvements:

  1. Add display methods of the selected table to the list of fields in the select dialog.

Credits:


The modification was inspired by a similar one performed by Nicolai Hillstrom for Dynamics AX 3.0. I moved it to DAX 4.0 and added some extra features I considered useful. One of them is implemented here (even thought I wasn’t able to download it and see how it is programmed).