There are few things to keep in mind when developing list page forms in Ax. This post aims to go through some of the key points. We'll start by looking at the sales order form (AOT name SalesTableListPage).
Sales order list page
The name of this form is SalesTableListPage, and is broken down as follows:
- Section 1 is the main list, implemented as a normal grid control.
- Section 2 contains several Parts, like 'Related information'
- Section 3 is an ActionPane, which contains button groups and buttons.
- Section 4 is also a 'Part'. The reason it's down at the bottom of the page and not on the right-hand side like the other is that it's PartLocation property is set to Preview.
Also note some of the general properties of the form:
- The FormTemplate property on the form is set to ListPage. Note that this adds special behavior and imposes restrictions on other properties within the form, such as not being able to add methods (see comment below).
- The query SalesTableListPage is used as the primary datasource on the form. When a query is added as the datasource it will automatically add the associated tables to the form. This structure is then fixed - ie tables cannot be removed or their join properties modified.
- The InteractionClass property is set to SalesTableListPageInteraction. This is a controller class that handles the majority of form logic. This is in place of coding up logic directly on active/write/init methods, and is a key part of how list pages are structured.
A nice feature of list pages is that they can be deployed to Enterprise Portal at pretty much the click of a button. This is the main reason that there is effectively no code attached directly to the form object - It all happens through the interaction classes and so can be shared between the rich client and web UI's. If you're interested, OpenERP does this with all it's forms - you structure it once, and it automatically deploys to a form and to the web (on Windows and Unix). This is an Ax blog though so I'll press on...
As mentioned, most if not all of the form logic for list pages is handled through an instance of SysListPageInteractionPageBase (selected via a form-property). The sales order form uses derived class SalesTableListPageInteraction. Some of the key methods you should know about include:
|initializing||Called when the form is initializing - Similar to the form init method|
|intializeQuery||Also called when the form is initializing - Similar to the datasource init method|
|selectionChanged||Called when the active record changes - Similar to the datasource active method.|
|setButtonEnabled||Should be overridden to dynamically enable/disable buttons based on the current selection. This is called from the selectionChanged method.|
|setButtonVisibility||Should be overridden to show/hide buttons when the form first opens. This is used more to do a one-off layout adjustment based on system configuration/parameters, as well as the menu-item used to open the form. eg If you have a menu-item that opens a form based on status, you may want to hide the relevant 'status' field to reduce clutter.|
Some of the key methods used in the list page interaction classes
These are just a handful - Have a look through SysListPageInteractionBase and other examples (like SalesTableListPageInteraction) to get a better idea of what's available.
You'll probably have realised that since we've now moved the logic off the form and into a separate class, we can now no longer access controls directly by name (using the AutoDeclaration property). This also applies to the datasources - Where we would've previously just referenced "SalesTable" to get the currently active sales order record, we now have to find another way.
It's a standard convention in Ax that controller classes obtain a reference to controls of interest when they're created. An example of this is the class LedgerJournalFormTrans. If you look at the class declaration you'll see member-variables that point to buttons and data controls, which are set when the class is instantiated. I've always found this a fairly tedious, if necessary, pattern - Fortunately the list page interaction classes provide helper functions for getting easier access to controls and data-context.If you look at the setButtonSalesOrder method in SalesTableListPageInteraction (which is called form setButtonEnabled), you'll see the code:
protected void setButtonSalesOrder()
this.listPage().actionPaneControlEnabled(formControlStr(SalesTableListPage, SalesCopyAllHeader), !salesTableInteractionHelper.parmReturnItem());
this.listPage().actionPaneControlEnabled(formControlStr(SalesTableListPage, SalesCopyJournalHeader), !salesTableInteractionHelper.parmReturnItem());
Obtaining a reference to a form control from the interaction class
What's happening here is we're getting a reference to the current ListPage (representing the form), and from that a reference to the control, identified by name. To get the current record, you'll be following the pattern (from method currentSalesTable):
return this.listPage().activeRecord(queryDataSourceStr(SalesTableListPage, SalesTable)) as SalesTable;
Obtaining current record from the interaction classAgain, this uses the listPage method to reference the form object itself, then uses the method activeRecord (accepting the datasource name) to return the currently selected/active record. activeRecord returns a generic record (instance of Common), so we need to cast it to the correct record type using the new 'as' keyword (familiar to you C# developers).
It would be a good idea to create wrapper methods for all of the record types you'll be referencing in the interaction handler. It's a shame we've lost the ability to reference the datasource directly, but it's a trade-off for getting one-click deployment to the web.
Info parts and context - Latest sales ordersThe 'related information' boxes use InfoParts to display fact-boxes and preview information. In the sales order form this includes general customer information for the current order, as well as a summary of the lines.
Let's have a closer look at the "Latest sales orders" part, and how it's attached to the form.
The Parts section of the form contains a reference to the menu item SalesLatestOrdersPart, which in-turn points to InfoPart SalesLatestOrdersInfoPart, as follows:
Referencing a part on a form - Object links
There are a lot of places in Ax2012 where we have to reference things indirectly through menu-items where a direct reference to the underlying object would probably do. In my opinion this makes things unnecessarily difficult to maintain - Same complaint for setting up new workflow types. I think the main reason is because the new security model is geared more towards setting privileges based on menu-items, but it still feels like overkill.The context of the main form is passed through to the part via the Datasource property and optionally the DataSourceRelation property on the form part reference. In this instance, the datasource is set to SalesTable, and the DataSourceRelation is set to EDT.SalesTable.CustAccount. What this does is take the current sales order record, pick up the CustAccount field (order account), and use that as the primary filter on the underlying query/table in the part (SalesLatestOrdersPart/CustTable).
The options available to you for DataSourceRelation are determined by finding compatible relations between the selected DataSource, and the primary table that is used in the part query. In this case, it finds the following relationships:
- SalesTable.InvoiceCustomer - Relation InvoiceCustomer defined on SalesTable
- SalesTable.OrderCustomer - Relation OrderCustomer defined on SalesTable
- EDT.SalesTable.CustAccount - Relation defined on extended data type CustAccount, used by field SalesTable.CustAccount.
- EDT.SalesTable.InvoiceAccount - Relation defined on extended data type CustAccount, used by field SalesTable.InvoiceAccount
Info parts and context - Preview pane (Sales lines)
The preview pane at the bottom works in a similar way but has a couple of differences worth noting:
- The PartLocation property is set to PreviewPane. This positions the part at the bottom of the page. The default setting of 'Auto' aligns it on the right. NB There is a deliberate convention for laying out list pages - The idea is to keep them all consistent across both the rich client and web.
- The DataSourceRelation is set to EDT.SalesTable.SalesID. The query used on the preview part (SalesTableListPagePreviewPane) uses SalesTable as the primary datasource, and joins to SalesLine. Setting the datasource relation to SalesID passes through the current sales order number which filters the part context automatically.
In addition, if you look at the layout section of the part (SalesTableListPagePreviewPane), you'll see two sections:
- SalesTable shows basic order header information.
- SalesLine shows the line details as a grid. Note the property 'Repeating' is set to true - This displays all matching records in grid form. I think this one could have been named more intuitively!
Cue groups and context - Related customer information
The related information section is similar in appearance to the InfoPart references, but actually points to a cue group, as follows (NB this diagram flows all the way through to the resulting form specified on the cue CustUnpaidInvoices, which is contained within Cue group CustRelatedInfo).
Since the datasource on the part reference is set to SalesTable, this is passed all the way through to the resulting form query (defined on CustOpenInvoicesListPage). This filters correctly because the main datasource of the cue (CustTransOpen) has a relation against SalesTable, based on the account number.
In my opinion, this is not a good design choice. It works, but logically CustTransOpen does not relate to SalesTable by account number alone. It looks like this has been added in to satisfy the cue relations, even though it's not strictly correct.
I think this is another area that is over-burdened with multiple relationships between objects through menu-items, and possibly a source of confusion when developing and maintaining cue references on forms. At this point I would probably lean towards using FormParts over cue groups for form layouts. As an example, the 'Open sales orders' cue seems to incorrectly filter on the currently selected order, making it a bit pointless. This looks like a side-effect of having to pass through too many objects and layers to propagate the context.
Modifying initial query through menu items
List pages can also be filtered automatically via properties on the menu-items. The form SalesTableListPage uses the query SalesTableListPage. The menu-item SalesTableListPage (ie "All sales orders") points to that form.
If you look at menu item SalesTableListPageJournal (ie "Sales orders of type journal"), you'll see that in addition it specifies query SalesTableListPageJournal. That query bases itself on the original query (SalesTableListPage) using the Composite query pattern, but specifies an additional range on the SalesType. This causes the list page to use that query instead of the default, and provides automatic filtering.
This is quite a handy way of doing providing different entry-points for similar views. In previous versions you would most likely have passed a parameter in via the menu item and updated the query in the datasource or form init methods.
Keep in mind that there are limitations when using Composite queries, like not being able to add additional joined tables. However you could get around this by updating the query in the initializeQuery method on the list page interaction class.
Hopefully this post helps people get an idea of the new 'list page' structure. Feel free to comment or leave questions.