SysLastValue is a handy framework in Dynamics 365 for Finance and Operations (D365FO). It saves the values you enter in forms for the current user within the current company so that the next time you open the form, your previous entries are already there. Originally introduced in AX 2012, this framework has been improved in D365FO with two new methods: getValue() and putValue(), making it easier to use.
SysLastValue: Old vs. New (simplified) framework
Before, using SysLastValue meant writing a bunch of different methods. The methods are hard to find in the documentation and are loosely coupled (e.g., no interface is implemented in standard D365FO), which means if you do not implement them, you will get an error in the runtime. Now, with the new methods putValue() and getValue(), you can save and retrieve values with just a single line of code.
Just as a reminder, previously we had to implement the following methods in our class:
- lastValueUserId()
- lastValueType()
- lastValueElementName()
- lastValueDesignName()
- lastValueDataAreaId()
- initParmDefault()
- pack()
- unpack()
- xSysLastValue::getLast(Object _caller)
- xSysLastValue::saveLast(Object _caller)
These values are accessible from Settings > User options > Usage data > All usage data and are stored in the SysLastValue table which is located in the AOT browser under the System Documentation > Tables node.
Now let's see how we can use the new way to store and retrieve values from the SysLastValue framework.
Storing values
To store values, use the putValue() static method from the xSysLastValue class.
Here’s how the method looks:
1 2 3 4 5 6 7 |
static public void putValue( container _value, CompanyId _company, userId _userId, UtilElementType _type, identifierName _elementName, identifierName _designName = '') |
Here's what each parameter means:
- _value: A container of values to store.
- _company: The company ID.
- _userId: The user ID.
- _type: The type of element (like Form, Class, Report, etc.). You can list all element types by opening the UtilElementType base enum under System Documentation > Base Enums in AOT browser.
- elementName: The name of the element.
- designName: An optional name for additional text if you need it (usually used to store the design name of the property/object to which the data belongs).
Retrieving values
To get your stored values back, use the getValue() static method from the xSysLastValue class. This method returns a container with the values you saved using the putValue() method.
Here's how the method looks:
1 2 3 4 5 6 |
static public container getValue( CompanyId _company, userId _userId, UtilElementType _type, identifierName _elementName, identifierName _designName = '') |
The parameters are the same as the ones you used with putValue().
How these methods work in practice
Now that we understand how the methods work, let’s look at an example of how they are used in practice.
These methods are used in the Docentric Print Management Utilities to check customer and vendor email addresses. For example, you can easily find customers without email addresses, ensuring your email print jobs run smoothly.
We used the new SysLastValue framework to filter out print management setting overrides by only selected customers. Here’s how you do it:
- Open the Print Management Utilities (go to Docentric AX workspace > Print management tab page > Utilities).
- Click on the Customer without email menu item, which opens a dialog.
- Use the Records to include filter to specify customers.
- Click the Show button.
At this point, the SysLastValue framework ensures that the filter remains the same the next time you open the dialog.
Below is the code showing how the query is stored in SysLastValue (see the selected line below):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
[Form] public class DocPrintMgmtUtilsEmailsDiagResults extends FormRun { private container filterQueryPacked; private const int CurrentVersion = 1; #localmacro.CurrentList filterQueryPacked #endmacro // ... OTHER CODE [Control("Button")] class CheckContactInfo { /// <summary> /// Depending on the caller menu item, run check for customers/vendors without Email contact /// or check for invalid customer/vendor Email contacts. /// </summary> public void clicked() { switch (callerMenuItem) { case menuItemDisplayStr(DocPrintMgmtUtilsCustomersWithoutEmailsDiagResults): element.customersWithoutEmails(); break; case menuItemDisplayStr(DocPrintMgmtUtilsCustomersWithInvalidEmailsDiagResults): DocPrintMgmtUtilsEmailsDiagTmp::getCustomerInvalidEmails(DocPrintMgmtUtilsEmailsDiagTmp); element.invalidEmailContacts(); break; case menuItemDisplayStr(DocPrintMgmtUtilsVendorsWithoutEmailsDiagResults): element.vendorsWithoutEmails(); break; case menuItemDisplayStr(DocPrintMgmtUtilsVendorsWithInvalidEmailsDiagResults): DocPrintMgmtUtilsEmailsDiagTmp::getVendorInvalidEmails(DocPrintMgmtUtilsEmailsDiagTmp); element.invalidEmailContacts(); break; default: DocGlobalHelper::handleException(strFmt('Invalid caller menu item (%1)', callerMenuItem)); } // Store the filter query to the SysLastValue table xSysLastValue::putValue(element.pack(), curExt(), curUserId(), UtilElementType::Form, element.name(), callerMenuItem); } } /// <summary> /// Packs an instance of this form as container. /// </summary> /// <returns>Packed container</returns> public container pack() { filterQueryPacked = filterQuery.pack(); // TIP: Always pack first the version and then the data. // This way you are safe for the future changes and you can read the right stored version. return [CurrentVersion, #CurrentList]; } } |
And here is how the query is retrieved from SysLastValue (see the selected line below):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
[Form] public class DocPrintMgmtUtilsEmailsDiagResults extends FormRun { private container filterQueryPacked; private const int CurrentVersion = 1; #localmacro.CurrentList filterQueryPacked #endmacro // ... OTHER CODE /// <summary> /// Sets the filter query used in the SysQueryForm form. /// </summary> private void setFilterQuery() { // Create the original filter query that is used when you click on the Reset button in the SysQueryForm form Query filterQueryOrig = new Query(); filterQueryOrig.title(filterQueryTitle); switch (callerMenuItem) { case menuItemDisplayStr(DocPrintMgmtUtilsCustomersWithoutEmailsDiagResults): case menuItemDisplayStr(DocPrintMgmtUtilsCustomersWithInvalidEmailsDiagResults): QueryBuildDataSource qbdCustTable = filterQueryOrig.addDataSource(tableNum(CustTable), tableStr(CustTable)); qbdCustTable.fetchMode(QueryFetchMode::One2One); break; case menuItemDisplayStr(DocPrintMgmtUtilsVendorsWithoutEmailsDiagResults): case menuItemDisplayStr(DocPrintMgmtUtilsVendorsWithInvalidEmailsDiagResults): QueryBuildDataSource qbdVendTable = filterQueryOrig.addDataSource(tableNum(VendTable), tableStr(VendTable)); qbdVendTable.fetchMode(QueryFetchMode::One2One); break; } // Pack the original filter query that is used when you click on the Reset button in the SysQueryForm form filterQueryOrigPacked = filterQueryOrig.pack(); // Count enabled data sources in the original filter query filterQueryOrigDataSourceCount = filterQueryOrig.dataSourceCount() - SysQuery::disabledDataSourceSet(filterQueryOrig).elements(); // Restore the filter query from the SysLastValue table this.unpack(xSysLastValue::getValue(curExt(), curUserId(), UtilElementType::Form, this.name(), callerMenuItem)); if (filterQuery == null) { // Create the filter query from the original filter query if it was not restored from the SysLastValue table filterQuery = new Query(filterQueryOrig); } // Initialize the 'Records to include' fast tab queryHelper = new SRSReportRunBuilder(this, null); QueryRun queryRun = new QueryRun(filterQuery); queryHelper.updateQueryGroupControl(QueryGroup, queryRun); // Set the 'Records to include' tab page caption this.setQueryTabPageCaption(); } /// <summary> /// Unpacks an instance of this form from container. /// </summary> /// <param name = "_packedForm">Packed container</param> /// <returns>True if unpacking was successful; otherwise false</returns> public boolean unpack(container _packedForm) { // First we read the version of the stored data. int version = conPeek(_packedForm, 1); // Then we unpack the data by the stored version. switch (version) { case CurrentVersion: [version, #CurrentList] = _packedForm; break; default: return false; } filterQuery = new Query(filterQueryPacked); return true; } } |
Conclusion
In this article, we have explained and demonstrated how to use the SysLastValue framework with just a single line of code. While the old way required implementing several additional methods, this new approach undoubtedly saves you time and is much more straightforward.
Great explination. Thanks.
Thank you very much sir! 😀
Always when we learn something new while developing our product, we want to share it with the community – because we truly believe that growing and sharing knowledge empowers us all in our community to do better what we do and what we are very passionate about!