Friday, October 28, 2011
Tutorial: AX 2012 - Invalid field access or Accessing unretrieved fields
As many of you have already seen in the new AX 2012 release, there are a lot more tables present in the application, because we went through an exercise of normalizing the data model.
Another feature that was added at the same time was the Table Inheritance, allowing to sub-class tables and add additional fields reusing some of the behaviors of the base table.
All of it is great! The only problem with it is that it comes at a price - we now need to retrieve a lot more data and do a lot more joins between tables. So we tried to mitigate that by using field lists where possible and adding AdHoc query support, which basically eliminates unneccessary joins between tables from the hierarchy, if fields from these tables are not selected.
We have also implemented a number of things that allow us to clearly see if the field was selected in the user interface, the APIs needed for doing the same, as well as special handling for invalid field access.
This tutorial contains a form, which showcases the different aspects of data access in cases described above.
You can download the project with the tutorial from my SkyDrive.
On the first tab, we have the standard "On hand" view, which in the data model is a join between InventSum and InventDim, with group by clauses on selected fields, in this case, ItemId from InventSum and InventLocationId from InventDim, and aggregation on the AvailPhysical column.
As you can see from the screenshot below, the rest of the InventDim fields are shown as Unretrieved. So, naturally, accessing one of them from code, for example, should not be a legal operation.
To verify that statement, there are 2 buttons on the form, the Incorrect and the Correct was of accessing the field. Clicking on the first one simply tries to read the value out of the InventSiteId field, while the second one uses the API method TableBuffer.isFieldDataRetrieved() to first verify if the field can actually be accessed.
Note, that in the below example, both buttons will work, as in, there won't be any errors shown to the user. When accessing the field, even though it is not retrieved to the client, the value will be treated as the default value for that type, that is, an empty string.
In order to verify that we do not access fields in an invalid way like above, we have introduced a parameter, that will throw an exception if a field that was not retrieved is being accessed.
In order to enable this validation, you need to update the below shown parameter in the Server configuration form (Located under System administration \ Setup \ System). After changing the value (note, that it is per AOS) you need to restart the AOS for the changes to take effect.
Now, if you try to use the first button (under Incorrect) from above, you will get a stack trace, notifying you that the specified field was not retrieved.
We suggest that when testing your modifications in the application, you always have that flag enabled, so as to avoid unpleasant and hard-to-find bugs later on in the production environment.
On the second tab of the Tutorial form, the same type of information is presented, only this time the output is actually based on a new table inheritance structure I created.
The base table, GenericBall, contains 2 characteristics of any ball. SoccerBall is extending it and adding an additional characteristic (for the sake of the example, let's assume Brand is only relevant to soccer balls). This is basically a very simple table inheritance structure.
In the form, however, I am only selecting to view the Brand field from SoccerBall, not selecting the other 2 fields from the base table. As expected, they show up as Unretrieved in the user interface.
However, there is one difference in how Scsc tables are handled - and that is, they will always throw an exception when you try to access one of the unretrieved fields.
The two buttons above the grid demonstrate that. Try it out, enabling/disabling the server configuration parameter shown above.
That's pretty much all there's to it. Let me know if you have any questions.
Now, I have one thing to ask you all too.
We in the AX Test team have done our best to find invalid field access problems before the release, but if you find one using the approach above, please log it either through the standard Microsoft channels (partner/ MsConnect/ etc.), or as a comment directly under this blog post.
Thank you!
Monday, October 24, 2011
Tutorial: AX2012 - A new way of accessing the QueryBuildDataSource for a particular FormDataSource
There are 2 new methods that were added to the FormDataSource class, which allow you to very easily and error-free access the underlying QueryBuildDataSource, whether that is for the initial form query or for the queryRun that also contains user filters and sorting.
So, for example, instead of writing:
this.query().dataSourceTable(tableNum(InventTableModule), 2);
this.queryRun().query().dataSourceName(identifierStr(Purch));
....
you can use the 2 new methods:
this.queryBuildDataSource();
this.queryRunQueryBuildDataSource();
It might seem as a very minor improvement, but in reality it greatly simplifies maintenance of code on forms, when new datasources are added or existing ones removed/renamed.
That's all for today.
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.
Wednesday, February 24, 2010
Casing and text search/comparison tutorial
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
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
Below is an explanation of the implemented functionality, in form of a Question/Answer section:
- 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 - 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". - 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. - 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. - 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())); - 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 - 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 7, 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 methodAOT\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
Monday, August 24, 2009
“Go to main table” on a RunBase dialog control
In one of my blog posts, I have been posed with the following question:
“how to achieve go to main table on dialog field.
for example a dialog is having a field of lookup custAccount, now what i need to have a go to main table functionality on the same.”
So, the short answer is: It is not possible to override jumpRef method directly on the dialog control. This is due to the fact that in order to show the “Go to Main table” link in the context menu of a control, you need to actually override that method on the form, which is later used to draw the context menu.
There are 3 possible ways that you can achieve the same functionality though, that I would like to briefly discuss here:
- Recommended Don’t bother with the dialog. Simply create a form, and override the jumpRef method on the needed control on the form. To demonstrate this behavior, you can use the Tutorial_RunBaseForm class and form. Override the jumpRef method, put the code to call the main form there. And you are done. Simple, and does not require a lot of coding. There is a lot of confusion, where junior AX developers think it will require a lot of code changes to replace a dialog with a form. This is not entirely that hard, as RunBase classes can use a regular AOT form instead of the regular dialog.
- Use controlMethodOverload approach, and create the controlName_context() method, which is executed when you try to open the context menu on the specified control. Inside this method, build the context menu from scratch. This way, only the items you want to be shown will be present in the context menu. Note: this means, that standard context menu items, like Setup, will not be shown in the menu (unless you add and handle them).
- Use controlMethodOverload approach, as in the previous example, but instead of overriding context() method, override the showContextMenu method. This will allow you to add your custom context menu items to the standard context menu. This does not seem to work on Axapta 3.0 though, and I did not try to figure out why. Note you need to uncomment some code in order to enable this approach (I left a TODO comment for that).
I have made the necessary changes, demonstrating the latter 2 approaches, using the class located under Classes\Tutorial_RunBaseBatch. You can download the xpo here (it contains only the USR layer changes).
The class used for drawing the context menu in the example is called PopupMenu. A good example of how this class can be used in the application is available in AOT under Forms\Tutorial_PopupMenu. Some information, along with another example, is available on MSDN.
A while ago, I have suggested some minor changes to the Dialog framework classes, which allow to simplify the process of overriding control event methods on the dialog. The original post can be found through this link: http://microsoftok.blogspot.com/2007/06/3-dialog-extensions.html
Let me know if any of this requires more clarification.
Sunday, April 26, 2009
Lookup methods tutorial + custom list lookup implementation
One of the great features available in Dynamics AX is the support of lookup forms that provide a user-friendly interface for selecting a value for a field from a list, and are highly customizable, allowing the developer great flexibility in meeting user needs.
Recently, I was posed a question of how to present a user with a list of custom-defined values in a lookup form.
The simple (and suggested) approach here is to create a new BaseEnum, containing all the specified values, and add a ComboBox control for this enumeration to the form. If the values are to be displayed conditionally (only a subset is shown each time), you can build a superset of all values in AOT, and use SysFormEnumComboBox class to limit the actual output at runtime.
But, this would not work in my case, because the values in the list were dependent on other data in the form plus the current date. Using a temporary table for this scenario seemed like an overkill (but possible). So i decided to investigate the system class SysLookup for hints on how this can be done with minimum effort. The implementation of this method is provided below.
As part of posting this implementation, I decided to also briefly describe some of the other lookup method options available to an AX developer, turning this post into a small tutorial on lookup methods.
1. The approach used most in the application is the implicit lookup based on table fields. As you can see from the control properties, DataSource and DataField specify which table field the control is based on. In my example, it is based on SalesTable.CustAccount, which uses the ExtendedDataType CustAccount, which has a relation to CustTable.AccountNum. Therefore, any time you add this field to a form, it will automatically provide a lookup button to select one of the customers.
Relations being specified on EDTs is at times confusing to people, who are used to seeing the relations between tables on the tables themselves. Technically speaking, such definitions on EDTs are incorrect. But no worries, AX supports the “correct” scenario out of the box as well. The relation to CustTable could have been specified on the table just as well.
2. 2nd most used approach is to specify the ExtendedDataType on the control directly. This is used to allow the user to specify a value to filter the records on directly from the form, for example (without the need to go to Extended Query form). It is also indirectly used on all RunBase dialogs in the system (when creating a DialogField, we specify the EDT to be used, which is transfered to the ExtendedDataType property on the corresponding control). In the tutorial, it is again the CustAccount EDT that is specified in the properties.
3. The 2 above examples both used metadata to define what lookup is to be displayed. The 3rd most used approach is relying on SysTableLookup class and builds the lookup at runtime. The code is relatively straightforward, and is described in more detail on MSDN. Using this approach, the developer can specify the query to filter the data being displayed in the lookup form. Note, that SysTableLookup only allows to have data from one table in the lookup form (+ display methods on this table). SysMultiTableLookup is an extension I have created a while ago, that adds this and other useful functionality to SysTableLookup class.
4. The remaining 2 standard approaches are rarely used in the application. Lookup based on ExtendedDataType is very similar to approach described under #2, but is executed from code at runtime. This way, you can change the lookup data displayed dynamically, based on some conditions (For example, on Ledger Journal lines, the offset account lookup shows vendors, customers, banks, etc. based on the offset account type specified on the line).
5. BaseEnum values are usually represented as a ComboBox (and, in special cases, CheckBox or RadioButton) control on forms in Dynamics AX. The lookup is provided by the kernel automatically for this type of control. But, sometimes, it is required to show the values of an enumeration in a string control – most common scenario is providing filter capabilities based on enums, where multiple values can be specified at once, similar to extended query form filters. In fact, this approach is actually used on SysQueryForm for enum fields. As you can see from the code, the lookup call is also very simple in this case.
6. This approach does not currently exist in the application, and the goals for its implementation were already described above. The lookup method code in this case looks rather straightforward, here it is:
public void lookup()
{
Counter yearCount;
List valueList = new List(Types::String);
for (yearCount = 0; yearCount < 5; yearCount++)
{
valueList.addEnd(strFmt("Year %1", year(SystemDateGet()) - yearCount));
}
SysLookup::lookupList(this, valueList, "List of years");
}
As you can see, the data displayed in this lookup depends on current system state (date) and other data (yearCount). It uses the class List to hold the values to be displayed.
You can download the project (Compatible with Axapta 3.0 – Dynamics AX 2009) with the tutorial and custom list lookup implementation by following the link below:
Note: The xpo contains changes to SysLookup class/form (only usr layer has been exported, for your convenience). Be careful when importing those, and don’t import them into the production environment.
Monday, September 22, 2008
SysMultiTableLoookup - dynamic lookups based on multiple tables
Anyway, this topic is really not about MSDN. It is about lookup forms.
First of all, for the record: I (and the Best Practices document as well) recommend creating custom lookup forms in AOT instead of dynamically coding them in the overridden lookup methods on controls/datasource fields.
But, in reality, this is true only for lookup forms with very large complexity. I won't go into a discussion of why that is the way it is here. :)
Now, back to what I was planning to write about:
In order to build a lookup form from code, developers use the SysTableLookup class.
You can go to MSDN (mentioned above) to read a How-to article on creating a run-time lookup form, as well as take a quick look at the SysTableLookup method descriptions.
SysTableLookup class has evolved over the multiple releases, providing more and more flexibility and control to the application developers.
I would like to publish another extension to this class, SysMultiTableLookup, which I hope will prove useful to members of the AX community.
Short list of features:
- Backward compatible, should cover everything that is present in AX 2009 version of SysTableLookup class
- Allows including multiple tables into lookups with different join types
- Completely based on the Query that you build, no extra parameters (except for the control) are needed to initialize the class
- Allows adding aggregated fields to the lookup
- Displays fields based on Boolean Enum as check boxes
- Allows to specify alternative labels when adding fields to the lookup
You can download the project from axaptapedia.com.
It has (to some extent) been tested on Axapta 3.0 SP3, AX 4.0 and AX 2009.
Also included in the project is a tutorial form, showing 4 examples of dynamic lookups using the new class. After importing the project, make sure to try out the form, and use it for future reference for code examples and other inspiration.
Wednesday, August 22, 2007
tutorial_Form_Dynalink (a small tutorial on dynalinks)
"In dynamics we have a Form Named "Form1" I created a button on it "Button1".
It will manage all the records related to the key in main form. There is dyna link created between the datasources. Only records of the currently selected main Key field will be shown. I need the selected KeyField value on the other form. How can I get it?"
So I wrote a small project to show the different possibilities.
In the project you will find 2 forms, 2 tables and a menuItem connecting the two forms.
3 ways of getting the current dynalink value are shown.
Here is a link to the xpo with the project:
download
Here is a screenshot of the result:
Forgot to give some explanations as well: :)
Basically, all the 3 form methods showing different ways of getting the dynalink value are called from the linkActive method in the second (slave) form.
This method is activated by the system every time a record in the parent form is changed, which allows for dynalinks to work. Great method :)




