Tutorial: Form “easy filters”

A customization type that pops up like a weeds in any given project is adding “easy filters” to forms. For some reason, users love em!*

Since I’ve done this bleeding super cool(!) customization so super-mega-ultra often, I’ve settled on a pattern that I like. But enough rambling! Here’s how I do “easy filters”.

Overview

1- Declare a form class-level variable, using the same datatype as the field to filter.

2 – Create an edit method that allows the user to modify the class variable.

3 – Modify the form datasource’s executeQuery() method to apply the value to the query before execution.

Reason why I like it

  • The code ends up in rather easy-to-see locations (rather than buried underneath controls [AutoDeclared controls, rawr!]).
  • The “filter” value can easily be modified from code.
  • Low impact on overall form design.

Walkthrough

This example will demonstrate modifying the Checks form [Cash and bank management > Common > Checks] to add an “easy filter” on the Date field.

Checks form

First step is to identify the datatype of the field we wish to filter on. To do that, we can identify the ‘Date’ field by [right-click on form > Personalize > find ‘Date’ control in the grid > System name], look up the table in the AOT, and check the field properties.

personalize form

As it turns out, the field uses the TransDate Extended data type. (If you wish the filter control to have a different label than the Extended data type, it is worth considering creating a custom Extended data type which extends TransDate with the label you desire. You can also just apply a label to the form control, and I probably wouldn’t tease you.)

Modify the form’s classDeclaration method to declare a variable for the filter value.

public class FormRun extends ObjectRun
{
 BankAccountTable bankAccountTable;
 boolean canPrintNonNegotiableCheque;
// Supreme filter --> 
 TransDate SupDateFilter;
// Supreme filter <-- 
}

Create an edit method in the form’s methods node.

// Supreme filter -->
public edit TransDate editSupDateFilter(boolean _set, TransDate _value)
{
 if (_set == true)
 {
     SupDateFilter = _value;
     BankChequeTable_ds.executeQuery();
 }
 
 return SupDateFilter;
}
// Supreme filter <--

Note the call to formDataSource.executeQuery(). This is what causes the form to update after the value is modified.

Create a DateEdit control in the form’s Design node. The easiest way is to drag the editSupDateFilter method into the Design node, although you can also create the DateEdit control manually and set the DataMethod property to “editSupDateFilter”.

Checks form AOT

Override the executeQuery() method on the BankChequeTable form datasource.

public void executeQuery()
{
// Supreme filter --> 
 QueryBuildRange SupTransDateR;
 
 SupTransDateR = SysQuery::findOrCreateRange(BankChequeTable_ds.queryBuildDataSource(), fieldNum(BankChequeTable, TransDate));
 if (SupDateFilter != dateNull())
 {
     SupTransDateR.value(SysQuery::value(SupDateFilter));
 }
 else
 {
     SupTransDateR.value(SysQuery::valueUnlimited());
 }
// Supreme filter <-- 
 super();
}

Note that the code is before the call to super(). This is because super() on a form datasource will execute the query, and the query range needs to take effect before that point.

Also notice the usage of SysQuery::findOrCreate(). This is superior (in this case) to QueryBuildDataSource.addRange() because the intention here is for there to be only one range on the TransDate field. This way there’s no need to override BankChequeTable_ds.init() or declare another form-level variable for a QueryBuildRange.

The reason for the if block is to handle the case where the date field is unspecified. In that condition, the range is cleared in order to display all records. The usage of SysQuery::value() is good practice any time values are getting put into a range, because it will handle datatypes correctly and will escape any special characters, so it avoids unexpected results due to e.g. wildcards. The SysQuery::valueUnlimited() call is just an explicit way to declare an “empty” (open-ended) range, since passing an empty string into SysQuery::value() will filter explicitly on blank values.

So that’s it. Easy filters, no problem.

Checks form filtered

And as expected, the query editor for the form looks like this:

Checks query editor

Other considerations

If the form gets launched from another form, it may be prudent to disable the “easy filter” functionality, because it would be unexpected from a user perspective to not see any records when launching the form via ‘View details’ from a different form. This mainly matters if the “easy filter” has a default value (rather than being open-ended).

It’s also worth considering “locking” the QueryBuildRange so the user cannot change it to a misleading value. That can be done with a single line (after the if/else block):

SupTransDateR.status(RangeStatus::Locked);

The flip side of that is that it makes it harder for a user to enter more advanced criteria, such as a range or comma-delimited list of dates. (Though it’s technically still possible if the user knows to type the field name in the Field value on the Query editor form.)

*I usually don’t think form “easy filters” are the best sort of customization to do because the functionality already exists (via Advanced Filter/Sort). It’s also easy to create unexpected results when the form is dynamically linked or when the user uses Advanced Filter/Sort in conjunction with the “easy filter”. If I can get my way I advise using the user-defined queries instead, though admittedly this sort of filter can be fairly convenient for the user.

Advertisements

Debugging security errors

Every so often, there is a security-related issue that pops up where the user can see the menu item, but when they launch it they get a security-related error.

A rather convenient way to troubleshoot that sort of thing is by temporarily turning SysAdmin off.*

First, assign the roles you’d like to troubleshoot to yourself.

From the development workspace, execute**:

SecurityUtil::sysAdminMode(!SecurityUtil::sysAdminMode());

 

And then launch a regular workspace. Now you should only see the menu items available to the roles assigned to you.

The beauty of this approach is that you can still catch breakpoints in the debugger to investigate what’s going on.

When you’re done, re-execute the above line to restore Admin mode. If you log out without restoring admin (or if the client crashes), you’ll automatically be back to admin the next time you log into Dynamics AX. (…or will you? dun-dun-dunnn)

*A fun little tidbit I learned from a co-worker here at Sikich just the other day when I encountered such an error.

**It’s usually better to not execute random code from the internet. All code on this blog is provided for educational purposes and if you mess things up, you should have done it in a test environment, so don’t come crying to me. (The code’s good though. For reals, you can trust me because I’m on the internet.)

Form datasource LinkTypes and what they do

TL;DR

Use Delayed if the form has two related grids of data.
Use InnerJoin or OuterJoin if the form has one grid displaying data from both form datasources.
Use ExistJoin or NotExistJoin if the form has one grid but there’s no need to display data from the related datasources.
Don’t use Passive or Active.

Explanation

I’ve interviewed quite a few Dynamics AX developers, and I’m often shocked by a common response I get to questions about how the LinkType property works on form datasources:

“Hm, I’m not sure. I just fiddle with that property until the form seems to act correctly.”

Wait, what?

The significance of LinkType is that it will determine what happens to the form’s queries when the form initializes. There are two important scenarios to understand:

1 – Delayed / Active / Passive: The form creates a query for each datasource.

2 – InnerJoin / OuterJoin / ExistJoin / NotExistJoin: The form creates a single query containing both datasources.

Scenario #1

When the “parent” datasource fires the init() method, it constructs a query for the parent datasource.

When the “child” datasource fires the init() method, it constructs a query for itself and creates a dynamic link to the parent datasource’s record. This causes the behavior of whenever the parent record selection changes, the “child” datasource executes its query.

DynamicLink

Scenario #2

When the “parent” datasource fires the init() method, it constructs a query for the parent datasource.

When the “child” datasource fires the init() method, it will modify the “parent” query to add itself as a child datasource.

InnerJoin

In both of these cases, it’s fairly easy to see what’s going on by going to [Right click on form > Personalize > Query tab].

The behavior of Delayed is to cause a short delay between the “parent” record’s selection and the child datasource’s query execution. This is for performance reasons. If, for example, a user is tapping or holding down an arrow key, there’s no need to select the related records until the user stays on a record.

In contrast, Active does not have such a delay. I have not yet found an appropriate reason to use this LinkType, because it seems to only be a performance decrease for the form.

Passive will never automatically fire the query, so really only has a use if the intention is to fire the child query from code. Conceivably, this could be used in conjunction with a button to fire the child query in cases where it’s somewhat long-running. I haven’t found a compelling need to use this LinkType either.

It should be noted that ExistJoin and NotExistJoin will not select the information from the datasource, and should only be used when the intention is to use the related table of data essentially as a “filter” for the parent datasource.

I hope this helps you avoid playing “property roulette” until the form starts behaving.

Financial Dimension backing entities

One fairly common modification request I’ve gotten since the onset of Dynamics AX 2012 is to create new Financial Dimension backing entities. Luckily, they’re fairly easy to create and can be a rather cost-effective customization.

I’m referring to the ‘Use values from’ field in General ledger > Setup > Financial dimension > Financial dimensions. (Or the DimensionDetails form for you nerds developers.)Financial dimensions formThe options in the ‘Use values from’ field allow a user to configure a Financial dimension to use values from a selection of records from a table in Microsoft Dynamics AX. The value of ‘Site’ is not out-of-the box, but here’s how to add it!


Note: naming convention is extremely important here, so pay attention or debug at your peril!

1 – Create a new view. It has to be named starting with ‘DimAttribute’DimAttribute view

2 – The DataSource has to be named ‘BackingEntity’

3 – There must be three fields named (exactly) ‘Key’, ‘Name’, and ‘Value’.

Key refers to the Record Id of the underlying table
Name refers to the long identifier or description of the record
Value is the user-friendly key value.

So in this case,

Key = InventSite.RecId
Name = InventSite.Name
Value = InventSite.SiteId

4 – Set some properties on the new view. This is more or less business as usual here if you’ve created custom tables and views before. The Label is important if you want user-friendly text (which you will, obviously) in the ‘Use values from’ selection.

DimAttribute view properties

 

The Backing entities get cached the first time the Financial dimensions form opens. (So even if you did everything correctly you may not see the new value.) In order to clear the cache so you see your new backing entity, create a class (or job if you’re a truly Supreme developer) and execute:

DimensionCache::clearScope(DimensionCacheScope::DimensionEnabledTypes);

(Or you could restart the client, then the AOS, then the Server, then delete your caches and so on and so forth. You know, the things you do when you’re not sure how things work.)

In case you need to debug to figure out what you messed up what step of the instructions you’ve missed, here’s the call stack for the Dimension Attribute creation when the Financial dimensions form is launched.

dimAttribute call stack

And the rest is just functional setup, which as we all know is not my job.