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.

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.