Thursday, September 23, 2010

Tutorial: Undocumented behavior of kernel functions min()/max()

I was recently reviewing some code written to be shipped with AX6, and noticed an unfamiliar pattern being used in it. I investigated a bit deeper, and turns out it actually works fine on previous versions of AX as well.


I am talking about 2 kernel functions for finding the maximum or minimum of the specified values.
The signature of these methods is shown on the below image:


As you can see, it takes 2 arguments of anytype, and returns an anytype which is the largest of the two values. But it can accept much more than 2 arguments, even though it is not documented as such.

I wrote a small job to showcase this behavior. The code is provided below. You can also download it from my SkyDrive Dynamics AX share.

static void Tutorial_MinMaxFunctions(Args _args)
{
    #define.ArraySize(11)

    Random  rand = new Random();

    int     counter;
    int     arrayInt[#ArraySize];
    str     arrayIntAsString;
    int     arrayIntMaxValue;
    int     arrayIntMinValue;
    ;

    for (counter = 1; counter <= #ArraySize; counter++)
    {
        arrayInt[counter] = rand.nextInt();
        if (arrayIntAsString)
            arrayIntAsString += ', ';
        arrayIntAsString += int2str(arrayInt[counter]);
    }
    info("Generated array of integers: " + arrayIntAsString);

    info("The typical way to find a maximum is by looping through all the values one by one, calling the comparison function multiple times");
    arrayIntMaxValue = minint();
    arrayIntMinValue = maxint();
    for (counter = 1; counter <= #ArraySize; counter++)
    {
        arrayIntMaxValue = max(arrayIntMaxValue, arrayInt[counter]);
        arrayIntMinValue = min(arrayIntMinValue, arrayInt[counter]);
    }
    info(strfmt("Max.value: %1 and Min.value: %2", int2str(arrayIntMaxValue), int2str(arrayIntMinValue)));

    info("Using max and min with 11 arguments works just as well");
    arrayIntMaxValue = minint();
    arrayIntMinValue = maxint();
    arrayIntMaxValue = max(arrayInt[1], arrayInt[2], arrayInt[3], arrayInt[4], arrayInt[5], arrayInt[6], arrayInt[7], arrayInt[8], arrayInt[9], arrayInt[10], arrayInt[11]);
    arrayIntMinValue = min(arrayInt[1], arrayInt[2], arrayInt[3], arrayInt[4], arrayInt[5], arrayInt[6], arrayInt[7], arrayInt[8], arrayInt[9], arrayInt[10], arrayInt[11]);
    info(strfmt("Max.value: %1 and Min.value: %2", int2str(arrayIntMaxValue), int2str(arrayIntMinValue)));

    info("Note that comparing an integer and a real also works, as well as outputing the results straight into an infolog message");
    info(max(12, 12.001));
}


Another interesting point is that it can actually accept different types of arguments, for example, a real and an integer, as shown above. And it actually returns an anytype, which implicitly gets converted to a string when sent to the infolog.

Disclaimer: Since this is not a documented feature, it can theoretically change in the future releases, but I doubt it in this particular case.

Wednesday, July 07, 2010

Advertisement: MDCC is looking for talent!

Hello, all.

Microsoft Development Center in Copenhagen has a number of open positions for Software Development Engineers in Test (SDETs), to work in the team responsible for shipping the latest version of the Microsoft Dynamics AX product.

Below is a detailed description of one of the currently open positions (SDET II role). Salary level and title are based on your education, number of years of experience, etc., nothing new here. Dynamics AX background is, of course, a plus (I assume this applies to all my readers).
The Microsoft Dynamics AX product group has an open position for a Software Development Engineer in Test (SDET) within our supply chain management teams. The position provides unique opportunities for professionals with a diverse background of business acumen and software engineering to work on one of the fastest growing Enterprise Resource Planning (ERP) products in the market.

Responsibilities:
Write and review technical requirements and design documents
Plan, design, and write code for automated tests of features within the supply chain management features of Dynamics AX
Create and use test tools and processes to both increase effectiveness in the daily work and assure quality of the product
Collaborate with other engineers to ensure all feature areas achieve the desired high level of innovation and quality our customers demand.

Requirements:
We are looking for engineers with a strong background in object oriented development. Experience with business applications or ERP solutions are pluses.

Software development experience, particularly within C#, C++ or similar object oriented programming languages
Strong technical and analytical skills
Excellent problem solving and design skills
Ability to work independently - and in teams
An excellent command of written and spoken English
Experience with Microsoft Dynamics AX or ERP products is a plus
Experience with X++ (a Dynamics AX language) is a plus
Software testing experience with an organized and structured approach is also a plus

We are also looking for less experienced people for the IAESTE student program, so all you excellent Computer Science students, interested in developing business applications and working for one of the world's leading software companies, welcome!

If you are interested in applying for the positions, please e-mail me your up-to-date CV at ivan.kashperuk(@nospam)hotmail.com

An additional request I would like to make is to leave a comment here, if you are invited to an interview, describing how it went, how you were treated, and what your impression was of the entire process, the MDCC campus, the interviewers, etc. This will help us make improvements in our hiring process, so I am really waiting for your comments. Note that anonymous comments are allowed.

Some additional information about MDCC:
Microsoft Development Center Copenhagen (MDCC) was created in 2002 following the acquisition of the Danish company Navision. Today, it has grown to be Microsoft’s biggest development center in Europe and a spearhead in the European IT industry. MDCC is Microsoft’s Center of Excellence for Supply Chain Management and drives the development of several of the Microsoft Dynamics ERP (Enterprise Resource Planning) products. Our products enable companies throughout the world to optimize the planning of their resources – and increase their revenues.

Today, the development center gathers around 650 people from more than 40 different countries. Every third employee is a non-Dane and that makes MDCC to one of the most international companies in Denmark. MDCC has been widely awarded for its unique work culture and is a coveted career booster for top talents from all over the world.

Thursday, July 01, 2010

Tutorial: Brief description of ways to close a form in AX

We had this question asked on one of the internal AX forums, and Michael Fruergaard wrote a short description of each method you can use.

Re-posting it here with some extra comments, so that new developers can read and understand, when to use what method.

There are “only” 5 ways to close a form:
  • Close - close the form. Similar to the 'X' button.
  • CloseOK – close the form, and set the OK flag – called by the Commandbutton::Ok
  • CloseCancel – close the form, and set the Cancel flag – called by the Commandbutton::Cancel
  • CloseSelectRecord – close the lookup form, and set return record
  • CloseSelect – close the lookup form, and set return value

The below methods (note their names are in past-tense) are used to determine if or how a form was closed:
  • Closed – Returns true, if the form is no longer open
  • ClosedOK – Return true, if the form was closed by the user clicking ‘OK’
  • ClosedCancel – Returns true, if the form was closed by the user clicking ‘Cancel’

Finally, CanClose() is called inside super() of any of the close methods. If CanClose() returns false, the form is not allowed to close.

Tuesday, May 04, 2010

Tool: DEV_SysTableBrowser and DEV_CreateNewProject tools now on AX 2009

3 or so years ago I created a couple of tools that provide extra capabilities for AX developers, specifically, ease up the creation of a new project with a predefined structure and name pattern, and add functionality to the way we browse tables with the standard SysTableBrowser, allowing to specify the exact fields you want to see, browsing temporary tables. Both tools were also available as Tabax plugins

You can read more about the tools, as well as Tabax, on the following pages on Axaptapedia:
DEV_SysTableBrowser home page on Axaptapedia
DEV_CreateNewProject home page on Axaptapedia

You can download the new versions of these tools through the pages on Axaptapedia, or directly from my SkyDrive: DEV_SysTableBrowser, DEV_CreateNewProject
If you are unfamiliar with these tools, I suggest you go through Axaptapedia, as it also contains a detailed description of the features provided.

A couple of things I would like to note:
  • There is a kernel bug in AX 2009 that heavily impacts the SysTableBrowser user experience. It takes around 10 seconds each time you want to browse the contents of a specific table. This bug was fixed with a hotfix rollup 3, so users of AX 2009 without the hotfix might have problems enjoying the DEV_SysTableBrowser tool.
  • In AX 2009 the behavior of SysTableBrowser changed a bit. Instead of re-opening the browser window each time the user changes from All fields to AutoGroup only, now 2 grids with these fields are added from the beginning, and hidden/shown based on user selection. DEV_SysTableBrowser provides much more flexibility when selecting the fields to display, so I was not able to follow the same approach. Thus, see bullet 1
  • I have not added any new AOT nodes to the template in DEV_CreateNewProject tool (Data sets, Report Libraries, etc). I don't think AX developers will invest that much time into SSRS reports and EP, and in these cases they will be able to manually create the remaining group nodes in the project.

Let me know if you have comments, suggestions or ideas for these tools.
Thanks

Thursday, April 22, 2010

Tool: User preferred startup menu for Dynamics AX 2009

In AX 2009, the user has no control over which menu is opened in the navigation pane and address bar when AX client is started. Most of the time you simply get the Home page, which is frustrating for a number of users. Another thing that is frustrating for them is the company account the application opens with. The latter can actually be setup using standard application functionality in the User options form. Just set the 'Start company accounts' to the account you want for the user by default. I wrote a small tool, that allows the user to select a preferred startup menu, ensuring that this menu is the one open every time AX client is started. Something similar existed in Axapta 3.0 application. You can download the xpo for this tool from my SkyDrive. Note, that it contains minor changes to a number of existing application objects. I suggest that you compare the xpo from the import dialog and ensure that you don't override any of your changes during import - the best thing would be to re-implement these minor changes manually. Below is a list of changes that I am referring to above:
  • SysUserInfo table - 1 new field was added
  • SysUserSetup form - 1 new control was added. Lookup method on datasource field overridden
  • Info class - startupPost method changed
In order to enable the functionality, simply select one of the menus in the User options, as shown in the below screenshot. Next time you start AX, the selected menu will be open by default. Start Menu in User options, Microsoft Dynamics AX 2009 Below is a short listing of "code patterns" used in the project, that can serve as examples for your future projects:
  • SysTableLookup - for displaying a lookup form with a list of menus
  • infolog.globalCache() - for storing a global reference to the navigator class
  • TreeNode iteration - for finding all menu references in MainMenu
  • QueryBuild classes - for constructing and filtering a list of menu references in MainMenu, used in the lookup form
  • infolog.addTimeOut() method - for scheduling execution of another method in a set period of time

Friday, March 26, 2010

Tutorial: refresh, reread, research, executeQuery - which one to use?

X++ developers seem to be having a lot of trouble with these 4 datasource methods, no matter how senior they are in AX.
So I decided to make a small hands-on tutorial, demonstrating the common usage scenario for each of the methods. I have ordered the methods based on the impact on the rows being displayed in the grid.
You can download the xpo with the tutorial on my SkyDrive.

1. Common mistakes

Often, developers call 2 of the mentioned methods in the following order:
formDataSource.refresh()
formDataSource.research()

or
formDataSource.reread()
formDataSource.research()

or
formDataSource.research()
formDataSource.executeQuery()

or
formDataSource.research()
formDataSource.refresh() / formDataSource.reread()

All of these are wrong, or at least partially redundant.
Hopefully, after reading the full post, there will be no questions as to why they are wrong. Leave a comment to this post if one of them is still unclear, and I will try to explain in more detail.

2. Refresh

This method basically refreshes the data displayed in the form controls with whatever is stored in the form cache for that particular datasource record. Calling refresh() method will NOT reread the record from the database. So if changes happened to the record in another process, these will not be shown after executing refresh().
refreshEx
Does a redraw of the grid rows, depending on the optional argment for specifying the number of the record to refresh (and this means the actual row number in the grid, which is less useful for AX devs). Special argument values include -1, which means that all records will be redrawn, and -2, which redraws all marked records and records with displayOptions. Default argument value is -2.
This method should be used sparingly, in cases where multiple rows from the grid are updated, resulting in changes in their displayOptions, as an example. So you should avoid using it as a replacement for refresh(), since they actually have completely different implementations in the kernel.
Also, note, that refreshEx() only redraws the grid, so the controls not in the grid might still contain outdated values. Refresh() updates everything, since this is its intention.

3. Reread

Calling reread() will query the database and re-read the current record contents into the datasource form cache. This will not display the changes on the form until a redraw of the grid contents happens (for example, when you navigate away from the row or re-open the form).
You should not use it to refresh the form data if you have through code added or removed records. For this, you would use a different method described below.
How are these 2 methods commonly used?
Usually, when you change some values in the current record through some code (for example, when the user clicks on a button), and update the database by calling update method on the table buffer, you would want to show the user the changes that happened.
In this case, you would call reread() method to update the datasource form cache with the values from the database (this will not update the screen), and then call refresh() to actually redraw the grid and show the changes to the user.
Clicking buttons with SaveRecord == Yes
Each button has a property SaveRecord, which is by default set to Yes. Whenever you click a button, the changes you have done in the current record are saved to the database. So calling reread will not restore the original record values, as some expect. If that is the user expectation, you as a developer should set the property to No.

4. Research

Calling research() will rerun the existing form query against the database, therefore updating the list with new/removed records as well as updating all existing rows. This will honor any existing filters and sorting on the form, that were set by the user.
Research(true)
The research method starting with AX 2009 accepts an optional boolean argument _retainPosition. If you call research(true), the cursor position in the grid will be preserved after the data has been refreshed. This is an extremely useful addition, which solves most of the problems with cursor positioning (findRecord method is the alternative, but this method is very slow).

5. ExecuteQuery

Calling executeQuery() will also rerun the query and update/add/delete the rows in the grid. The difference in behavior from research is described below.
ExecuteQuery should be used if you have modified the query in your code and need to refresh the form to display the data based on the updated query.
formDataSource.queryRun().query() vs formDataSource.query()
An important thing to mention here is that the form has 2 instances of the query object - one is the original datasource query (stored in formDataSource.query()), and the other is the currently used query with any user filters applied (stored in formDataSource.queryRun().query()).
When the research method is called, a new instance of the queryRun is created, using the formDataSource.queryRun().query() as the basis. Therefore, if the user has set up some filters on the displayed data, those will be preserved.
This is useful, for example, when multiple users work with a certain form, each user has his own filters set up for displaying only relevant data, and rows get inserted into the underlying table externally (for example, through AIF).
Calling executeQuery, on the other hand, will use the original query as the basis, therefore removing any user filters.
This is a distinction that everyone should understand when using research/executeQuery methods in order to prevent possible collisions with the user filters when updating the query.

Thursday, March 04, 2010

Highlighting your code on Blogger

A couple of people have now asked me for the tool that I use for pasting the source code on my blog.
In order to use the same, you will have to change the template of your blog and include the following scrips:


<script src='http://amalafe.googlecode.com/svn/trunk/Scripts/shCore.js' type='text/javascript'/>
<script src='http://amalafe.googlecode.com/svn/trunk/Scripts/shBrushCpp.js' type='text/javascript'/>
<script src='http://amalafe.googlecode.com/svn/trunk/Scripts/shBrushCSharp.js' type='text/javascript'/>
<script src='http://amalafe.googlecode.com/svn/trunk/Scripts/shBrushSql.js' type='text/javascript'/>
<script src='http://amalafe.googlecode.com/svn/trunk/Scripts/shBrushXml.js' type='text/javascript'/>
<link href='http://amalafe.googlecode.com/svn/trunk/Styles/shCore.css' rel='stylesheet' type='text/css'/>
<link href='http://amalafe.googlecode.com/svn/trunk/Styles/shThemeDefault.css' rel='stylesheet' type='text/css'/>
<script type='text/javascript'>
SyntaxHighlighter.config.bloggerMode = true;
SyntaxHighlighter.config.clipboardSwf = 'http://amalafe.googlecode.com/svn/trunk/Scripts/clipboard.swf';
SyntaxHighlighter.all();
</script>


After that, you will be able to create code by specifying the following:

<pre class="brush: c-sharp;">
Your code goes here...
</pre>

Wednesday, February 24, 2010

Casing and text search/comparison tutorial

I have recently received a question from one of my blog readers.
He was asking about the possibility of doing case-sensitive search in AX.

I would like to reply to that by creating a small tutorial on search in AX. It is definitely not going to cover all the scenarios, but will give beginners a basic understanding of their options.

Download the tutorial xpo from my Skydrive

The tutorial consists of 1 form with 3 tab pages.

1.
Comparing two strings in AX is very simple: You can basically use the equality operator "==". As you can see from the tutorial, this is the case-insensitive operations, so "vanya" is equal to "Vanya".
AX also supports case-sensitive comparison. Kernel function strCmp() compares two strings taking into account the casing of the symbols in the string.

2.
Searching for a substring is a common operation in AX.
For that you have a number of options, as usual:
strScan() function ignores casing and allows you to specify from which position to search, and how many symbols. This is very basic, and a method like this is present in any programming language.
TextBuffer.find() is a more advanced use of the search mechanism. First of all, it allows to ignore or take into account the casing in the source text. Similar to strScan, it allows to specify the start position for the search.
What it also has is support for Regular Expressions, as well as the ability to, for example, paste the text to Windows clipboard.
Lastly, there is the match() function. The main purpose of it is to find a match based on the specified pattern using regular expressions, but nothing prevents using it for a simple search operation. It has a rather limited output though. You only get a boolean value stating whether a match was found or not, while with the previous 2 methods you also get the position of the substring.

Interesting
An interesting discovery that I made when writing the tutorial was about the speed of the different search operations. I have included this into the tutorial, so you can go in and try it yourself on your specific setup.
On my box, strScan() was the slowest operation of all, while TextBuffer, which I considered to be a very heavy class, was performing rather well.

Of course, single operation time compared to database operations is very low, so you won't notice it in your daily work. But it is something to think about.

3.
Finding a symbol in a string based on a specified set of symbols is also possible.
You have 2 functions at your disposal for that: strFind and strNFind. The difference is that strNFind searches for any symbol NOT present in the provided set, compared to strFind.


This is not an extensive list, so I would be interested in hearing which functions you use or what your results for performance comparison would be.

Tuesday, February 16, 2010

UtcDateTime in Dynamics AX 2009

In Dynamics AX 2009, Microsoft introduced a new data type, UtcDateTime, that is going to eventually replace the 2 existing types, Date and Time, which are still present in the application right now.
Obviously, the introduction of this new type requires a tutorial on how it can be used on forms, how you can filter on fields of this type, as well as what functions are available out of the box for it.
So I have made such a tutorial, and I hope it will be useful for developers upgrading to AX 2009.

Download the xpo for the tutorial from my SkyDrive

The tutorial consists of a single form, containing the following elements:
  • a grid, displaying data from CustTable
  • 4 buttons for various filtering actions
  • 3 controls that allow specifying the filtering conditions for the data
. In the form, you can see how UtcDateTime based controls are displayed both in a regular group and in a grid.

Dynamics AX UtcDateTime tutorial form

Below is an explanation of the implemented functionality, in form of a Question/Answer section:
  1. Q: Can I filter on the new UtcDateTime type, specifying the Date part only?
    A: Yes. You simply have to specify only the date part when applying the filter, like below. Note, that this also works fine when filtering directly from the UI (Ctrl+F).
    qbdsCustTable.addRange(fieldNum(CustTable, CreatedDateTime)).value(queryValue(DateFilter.dateValue()));
    What is interesting is how the kernel processes this range. In the below infolog, you can see that when viewing the query, it displays a "==" condition on a specific dateTime value.
    But in reality, as you can see from the SQL trace, a range ">= && <=" condition is applied to span exactly one day.
    Also note, that the values in the trace are displayed accounting for the TimeZone I am in, as well as for Daylight Saving Time

    SQL trace for Date filter on UtcDateTime field
  2. Q: Can I filter on the new UtcDateTime type, specifying the Time part only?
    A: No, this is not possible with a UtcDateTime type. The range applied when specifying a Time value is the minimum DateTime value, as seen below. Note, that in the SQL trace it is converted to "no range".

    SQL trace for Time filter on UtcDateTime field
  3. Q: Can I use similar query functions for UtcDateTime type?
    A: Yes. All the main existing functions for working with QueryBuildRange also support UtcDateTime. For example, in the infolog below you can see how a range on 2 UtcDateTime dates is applied. Global::queryRange method was used to achieve that. Note, again, that the SQL trace offsets the DateTime by the appropriate number of hours based on my location.

    SQL Trace for UtcDateTime range
  4. Q: How is the UtcDateTime stored in the database? Is it displayed the same way on forms?
    A: The UtcDateTime fields are in the database always stored in Coordinated Universal time (UTC). Whenever displayed on forms and bound to table fields, the data is converted to the user's preferred timezone. Note, that you need to take care of the conversion yourself, if the control is not bound to a field. For an example, see the init method of the tutorial form.
  5. Q: What standard helper functions are present for working with UtcDateTime type in the application?
    A: The main entry point for working with UtcDateTime type is the DateTimeUtil class. It allows adding Days/Months/Years, as well as applying an offset, getting the user's preffered timezone, converting from/to other types, etc. An example from the form init method is posted below:
        // getSystemDateTime() returns the current DateTime set in the system, not the current machine dateTime.
        // Note that getSystemDateTime() returns a UTC date and time, not your local date time.
        // In order to receive your local DateTime value, you should use methods applyTimeZoneOffset and specify the preferred time zone.
        utcDateTimeFilter.dateTimeValue(
            DateTimeUtil::applyTimeZoneOffset(
                DateTimeUtil::getSystemDateTime(),
                DateTimeUtil::getUserPreferredTimeZone()));
  6. Q: Does this mean that the support for Date and Time types has been removed?
    A: No, Date and Time are still supported. As you can see in the form init method, SystemDateGet(), timeNow(), today() are all still supported
  7. Q: I don't see the actual filter values in the SQL log. Instead, all I see are "?"'s. Also, how can I limit the number of data/fields selected from the database?
    A: This is just some extra stuff, not related to UtcDateTime, but still useful to know and pay attention to.
    CustTable has a very large number of fields, and I am only displaying 4 of those in the form, so it would be unwise to always query and return all of the fields. Luckily, the datasource has a property OnlyFetchActive, which controls the query behavior by only selecting the fields actually displayed on the form. Note, that you should avoid using this with editable datasources. See comments to this post for details
    As for "?"'s in the SQL trace - that is happening due to the use of placeholders. This in general optimizes the performance of the queries, by creating a query execution plan and storing it for future use. But it is possible, and is required in some specific cases, to force the use of literals (meaning the actual values of the ranges in the query). This can be done using the literals method on the query. See method init on the form for an example.

Sunday, February 07, 2010

Performance optimization: Deleting inventory journal lines

Preamble

As a developer, you should always consider performance implications of the code you write. In an ERP application like Microsoft Dynamics AX, the main focus should be on query execution, since it takes up the overwhelming part of the servers' resources.

You should always write queries that would execute the minimum amount of time and use the minimum amount of resources, at the same time producing the expected output in all cases.

Note, that performance is one of those things you cannot really verify on a 1-box install with a small test dataset. Most query problems show up only when tested with many users concurrently loading the AOS on a large-size database.

Code example

Table method
AOT\Data Dictionary\Tables\InventJournalTrans\Methods\delete
contains the following code:

if (this.Voucher)
{
if (this.numOfVoucherLines() == 0)
JournalError::deleteVoucher(tablenum(InventJournalTable),this.JournalId,this.Voucher);
}

The code is logically correct, deleting related records from the JournalError table, which contains error messages generated during validation and posting of journals.
But now let us consider the actual implementation. If we rephrase the conditions under which we delete the error message history, it would sound something like:
If a Voucher number is specified on the line being deleted, and there is no more lines in this journal that use this Voucher number, then we remove the JournalError records.

Deeper code analysis for performance

So, let's go in deeper, and open the code for method numOfVoucherLines:

Integer numOfVoucherLines()
{
return any2int((select count(RecId) from inventJournalTrans
where inventJournalTrans.JournalId == this.JournalId &&
inventJournalTrans.Voucher == this.Voucher).RecId);
}

As you can see, it counts all the journal lines with the specified Voucher number and JournalId.
Does it generate the expected output? Yes.
Is it optimal for the scenario in which it is used? No.

In order to determine if JournalError records can be deleted, we only need to know if at least 1 record with the specified Voucher number exists. So why do we need to search and count all such records? We don't.
Note that this code is being executed for each journal line, magnifying the impact of a non-optimal query N-fold.

Suggested solution

So, if we create a new method like this:

boolean voucherLineExist()
{
return (select firstfast firstonly forceplaceholders recId from inventJournalTrans
index hint VoucherIdx
where inventJournalTrans.journalId == this.journalId &&
inventJournalTrans.voucher == this.voucher).recId != 0;
}

and use it instead of numOfVoucherLines() in delete() method, we will greatly improve the overall performance when deleting journal lines.
It has been measured, that the time it takes to delete a journal with a large number of lines (88000, to be specific) has reduced by 6 times, which is fantastic.

Notes

  • forceplaceholders is used in the query explicitly to make sure a query plan is created and reused for this query, which further improves the performance, since the Voucher number most probably changes from line to line (depends on related journal name setup).
  • It is generally still a best practice to avoid creating inventory journals with such a large number of rows, because the journal is usually processed in 1 database transaction.

See also

Thursday, January 28, 2010

Lookup form returning more than one value

I have been posted with the question of creating a lookup that would return more than 1 value into the calling record, filling in a number of fields and populating the selected control with the corresponding value at the same time.

I will try to answer that question by an example from the standard application, particularly using one of the forms owned by my (Inventory) team.

Example


If we take a look at the lookup form for one of the item inventory dimensions (Configuration, Size or Color), we'll see the following tab page on it:
(assuming more than 1 item dimension is enabled for this item)



Basically, the user is able to select the configuration that belongs to a specific dimension combination. Note the check-box "" under the grid. If the user sets that check-box and then selects the configuration from a combination, the values for all item dimensions will be copied to the record from the parent form.
So, basically, the lookup returns more than just the configuration dimension value, but also size and color.

How is that implemented?


The lookup is implemented using a custom form, present in AOT, with name ConfigIdLookup.

The methods important for a working lookup are:

  • init() method.

In particular, the way the calling control is determined based on the args passed into the form:


callerControl = SysTableLookup::getCallerStringControl(element.args());

  • selectMode() method.

(called from setSelectMode() on this form). This method accepts an AX form control as parameter, and informs the kernel about which control should be used as the returning control.

  • closeSelect() method.

This method is executed on lookup forms wheneven a selection of a particular value is made. So, obviously, in this method you still have access you all the fields of the datasource with the selected record, as well as to the calling form through element.args(). Here is the code that handles the update of multiple fields:


if (ctrlTabPageCombination.visible() &&
selectAllCombi &&
ctrlTabPageCombination.isActivePage())
{
// Genericly determine the InventDim datasource on the calling form
callerInventDimDS = inventDimFormSetup.callerInventDimFormDatasource();
if (callerInventDimDS)
{
//...
// Get the current record of that datasource
callerInventDim = callerInventDimDS.cursor();
// Set the needed fields on the record based on the selection in the lookup form
// Basically, this is how you can achieve returning more than one value from a lookup
// Get the record where the values need to be put in, and put them in from the currently selected record
if (inventDimCombination_ConfigId.visible() && inventDimCombination.ConfigId)
callerInventDim.ConfigId = inventDimCombination.ConfigId;
if (inventDimCombination_InventSizeId.visible() && inventDimCombination.InventSizeId)
callerInventDim.InventSizeId = inventDimCombination.InventSizeId;
if (inventDimCombination_InventColorId.visible() && inventDimCombination.InventColorId)
callerInventDim.InventColorId = inventDimCombination.InventColorId;
// Refresh the datasource so that values are displayed on the calling form
callerInventDimDS.refresh();
}
}



If you want to know more


  • Spend some time investigating the existing methods on SysTableLookup class, like the method getCallerStringControl, filterLookupPreRun_DS, filterLookupPostRun.

  • Look at the methods in InventDimCtrl_Frm_Lookup class. They handle all the logic of looking up inventory dimensions, and contain some nice examples of traversing the calling object.

  • Investigate, how the lookup form described above prevents the closing of the form when the check-boxes are clicked or tab pages changed. Hint: take a look at the usage of canSelect form variable

Tuesday, January 26, 2010

Short posts about AX every day

OK, so here is another one of those "A new blogger in town" posts.

But I couldn't resist, since I really like the idea of posting some little useful things about AX on a daily basis (weekends excluded, thank God!)

So I suggest all of you go and check it out for yourself.

The link is:
http://axdaily.blogspot.com/

Thursday, January 14, 2010

Run AX as a different user from Windows Explorer

This is something useful for people who have already switched to Windows Server 2008 or Windows Vista / Windows 7.
As you have probably noticed, the Run as... command is not available in these versions of Windows any longer (it's still available from command line, of course, but that's not as user friendly).

As AX users/developers/etc., we all ofter have to log into AX as a different user, for example to verify security settings for a particular AX role being setup.
Having Run as... command in the context menu really saves time here.

So, Microsoft, namely Mark Russinovich, provided a way to return this useful command back into the standard context menu.

You can download and install ShellRunas from technet.

Now, to install it, simply follow the easy instructions below:
  1. Download and unzip ShellRunas by following the link above
  2. Copy ShellRunas.exe to your Windows\System32 folder
  3. Open a Command Line and run the following command: shellrunas /reg
  4. A message box confirming successfull installation should pop up. Click OK
  5. Now, holding down the SHIFT key, right-click the AX icon. You will see a Run as different user... item in the context menu


To uninstall the Shellrunas utility, simply execute the following command from a command prompt: shellrunas /unreg

Tuesday, January 05, 2010

Editor script for simplifying the search in AOT

When doing code refactoring, or when investigating a particular class or form to understand the functionality behind it, it is often handy to search for variables used in its methods, looking at where and how each variable is used.

It is tedious to manually go through the steps of copying the name of the variable into the clipboard, opening up the parent class/form, opening up the AOT Find dialog for it, pasting the variable name into the Selected Text field and pressing Find now.

I have automated these steps, putting the code into an editor script.

You can download the xpo, which contains the EditorScripts class with the additional method, by following this link.

Now you should be able to very easily and quickly find the required code.

The code of the editor script:

///
/// Editor script for finding all occurrences of the selected text in the parent node of the selected method
///

///
/// Reference to the AX editor
///
///
/// 2010-01-05, ivanv
///

void addIns_FindAllOccurrencesInNode(Editor e)
{
#AOT
TreeNode treeNode;
FreeText selectedText;

FreeText GetSelectedTextFromEditor(Editor _e)
{
str 1 curSymbol;
int iCopyFrom;
int iCopyTo;
FreeText _selectedLine;
;
if (_e.markMode() != MarkMode::NoMark && _e.selectionStartCol() != _e.selectionEndCol())
{
_selectedLine = strLRTrim(subStr(_e.currentLine(), _e.selectionStartCol(), _e.selectionEndCol() - _e.selectionStartCol()));
}
else
{
_selectedLine = _e.currentLine();
for (iCopyFrom = _e.columnNo()+1; iCopyFrom >= 0; iCopyFrom--)
{
curSymbol = subStr(_selectedLine, iCopyFrom, 1);
if (!strAlpha(curSymbol) && curSymbol != '_')
break;
}
for (iCopyTo = _e.columnNo()+1; iCopyTo <= strLen(_selectedLine); iCopyTo++)
{
curSymbol = subStr(_selectedLine, iCopyTo, 1);
if (!strAlpha(curSymbol) && curSymbol != '_')
break;
}
_selectedLine = (iCopyFrom < iCopyTo) ? subStr(_selectedLine, iCopyFrom + 1, iCopyTo - iCopyFrom - 1) : '';
}
return _selectedLine;
}

void FindLinesContainingSelectedText(FreeText _selectedLine)
{
Args args = new Args(formstr(SysAOTFind));
FormRun sysAOTFindFormRun;
FormStringControl containingTextCtrl;
FormButtonControl findNowBtnCtrl;
;

sysAOTFindFormRun = classFactory.formRunClass(args);
sysAOTFindFormRun.init();
sysAOTFindFormRun.run();

// Set the text to find
containingTextCtrl = sysAOTFindFormRun.design().controlName(identifierStr(ContainingText));
containingTextCtrl.setFocus();
containingTextCtrl.pasteText(_selectedLine);

sysAOTFindFormRun.detach();

// Launch the search process
findNowBtnCtrl = sysAOTFindFormRun.design().controlName(identifierStr(FindNow));
findNowBtnCtrl.clicked();
}
;

selectedText = GetSelectedTextFromEditor(e);
if (selectedText)
{
// Find the currently open method node
treeNode = TreeNode::findNode(e.path());
// Find the parrent node of the method
treeNode = TreeNode::findNode(xUtilElements::getNodePathRough(xUtilElements::parentElement(xUtilElements::findTreeNode(treeNode))));
if (treeNode)
{
treeNode.AOTnewWindow();

FindLinesContainingSelectedText(selectedText);
}
}
}