This guide describes the process in how to convert a nested child table (child tables that has been ported from the Centura code using IcePorter) into control based child tables (which is the standard way .NET serializes and handles objects). The goal is to have this conversion done in all child tables for the Next Core release of IFS Applications (IFSAPP9), getting completely rid of the ported version.
All the code (original and refactored) that is mentioned in this guide can be found in the ORDER component, being found in the zipped file Example – Order.zip
The result after a conversion will be having following files modified:
<Form>.cs (ex: frmInvoicableLinesPerOrder.cs)
<Form>.Designer.cs (ex: frmInvoicableLinesPerOrder.Designer.cs)
<Form>.resx (ex: frmInvoicableLinesPerOrder.resx)
<Component>.lng (ex: Order.lng)
Open the solution
Open the solution with the form hosting the nested child table and view its source/design.
Example:
Solution: Ifs.Application.Order_.sln
Form: frmInvoicableLinesPerOrder.cs
The region tblInvoicableLine contains the nested child table class that will be refactored.
If possible, remove obsolete/unused code from the nested class. Typically,
instance variables (#region
Window Variables) in the nested class can
quite often be refactored and moved being placed locally inside a method,
instead of being instance variables on the form and available from all different
locations.
Beware: Instance variables can be used as bind variables inside strings so using “Find All References” might not give you the complete answer. Therefore, you need to search for instance variables you’re about to refactor using simple text search as well to ensure they are not used as bind variables anywhere. If they are, you will need to leave them as they are for now, we’ll get back to them later again.
Example
public SalNumber nRow = 0; Never used, can be removed completely! public SalNumber nBaseCurrRounding = 0; Used as a bind variable, keep it for now! public SalArray<SalString> sItemNames = new SalArray<SalString>(); public SalArray<SalWindowHandle> hWndItems = new SalArray<SalWindowHandle>(); Used only in the method colsFeeCode_OnPM_DataItemZoom, move them to that method as local method variables:
Before
privatevoid colsFeeCode_OnPM_DataItemZoom(object sender, WindowActionsEventArgs e) { #region Actions e.Handled = true; if (Sys.wParam == Ifs.Fnd.ApplicationForms.Const.METHOD_Inquire) { e.Return = Ifs.Fnd.ApplicationForms.Var.Component.IsWindowAvailable("tbwStatFee") && Ifs.Fnd.ApplicationForms.Var.Security.IsDataSourceAvailable("tbwStatFee"); return; } else if (Sys.wParam == Ifs.Fnd.ApplicationForms.Const.METHOD_Execute) { this.sItemNames[0] = "COMPANY"; this.hWndItems[0] = this.colsCompany; this.sItemNames[1] = "FEE_CODE"; this.hWndItems[1] = this.colsFeeCode; Ifs.Fnd.ApplicationForms.Var.DataTransfer.Init("STATUTORY_FEE", this, this.sItemNames, this.hWndItems); SessionNavigate("tbwStatFee"); e.Return = true; return; } #endregion }
After
privatevoid colsFeeCode_OnPM_DataItemZoom(object sender, WindowActionsEventArgs e) { SalArray<SalString> sItemNames = new SalArray<SalString>(); SalArray<SalWindowHandle> hWndItems = new SalArray<SalWindowHandle>();* #region Actions e.Handled = true;* if (Sys.wParam == Ifs.Fnd.ApplicationForms.Const.METHOD_Inquire)* { e.Return = Ifs.Fnd.ApplicationForms.Var.Component.IsWindowAvailable("tbwStatFee") && Ifs.Fnd.ApplicationForms.Var.Security.IsDataSourceAvailable("tbwStatFee"); return; } else if (Sys.wParam == Ifs.Fnd.ApplicationForms.Const.METHOD_Execute) { sItemNames[0] = "COMPANY"; hWndItems[0] = this.colsCompany; sItemNames[1] = "FEE_CODE"; hWndItems[1] = this.colsFeeCode; Ifs.Fnd.ApplicationForms.Var.DataTransfer.Init("STATUTORY_FEE", this, sItemNames, hWndItems); SessionNavigate("tbwStatFee"); e.Return = true; return; } #endregion }
This step is only necessary if any of the two rules below are true:
The form window hosts two or more child tables
Variable and/or method names inside the child table are named the same way as variable/method names on the form.
Example
:The form window contains an equally named variable like
Since we will need to move the code from the nested class into the form window, these two variables will then result in duplicate declarations, which is something we need to handle. Due to that, we rename the variable in the nested class to <table_name>_<variable_name> like:
public
SalString tblInvoicableLinesTableWindow_sStmt = "";Hint: By using the “Rename” operation in VS, all the strongly typed references to that variable will automatically be updated.
Open the Designer view for the form window and select the child table. Click the RMB and run the “Serialize as control…” action.
An info box named “Serialization Result” will show you a general description on what happened
By clicking on the “View Details” link should display a detailed log on the steps that were made like:
Rename the nested child table to “OLD_<table_name>
Move it to the upper left corner
Sizing it to a small child table
Creating a new child table control , named <table_name>
Creating new columns and connecting them to the child table control, naming them <table_name>_<column_name>
Translation path changed from <table_name>.<column_name> to <table_name>_<column_name> in language file.
And some more…
Press "Close".
On the form window, you should now have two child tables, the
old “small” one and a new one. The new child table should have all the attributes set to the same values as its original control (the nested child table) used.The only purpose with keeping the
old child table is in case you want to verify the serialization and debug any issues that could arise during the serialization. You can then easily (well, quite easily that is) compare the two child table.Most likely, you will not need the
old child table any more. Therefore, mark it in the designer and delete it, only keeping the new child table.The source code in the nested child table class must now be moved to the form window and refactored. Do that like this:
Create a new region named “ChildTable - <table_name> in the form window, just about the region for the nested child table class.
Move all regions, excluding
Constructors/Destructors and System Methods/Properties from the nested child table class to the new region.What’s now left in the old region should only be some standard generated stuff that we no longer need and can therefore delete completely.
Mark the entire region for the nested child table class and delete it.
View the *.designer file for the form and find the same region inside
it. Delete it as well.
By now, there should not be any traces left of the
old nested child table apart from potential references using its name. Therefore, we need to find and replace any such references to the new child table control.Having the form window source code window open, use the “Find and Replace” dialog entering the “OLD_<table_name>” name in Find what field and the <table_name> in the Replace with field.
Set the Look in to Current Document and press Replace All.
Compiling the project will generate some errors now. Depending on how much code that was moved and of course what kind of code, the amount of errors will differ for each child table. The error list can now be used as a “ToDo” list where you correct each error, one by one.
Some of the typical steps that need to be done here are listed here:
Renamed references to columns
All the columns were renamed so you need to correct all the references
to them.
(Hint: Double click each column reference that is wrong, selecting it as marked, then hit CTRL + SPACE. That should bring up the IntelliSence with a suggestion to the correct column.)
Example
:Before: public SalNumber CalculateLineTotal() { #region Actions using (new SalContext(this)) { if (colnCurrRate.Number != Sys.NUMBER_Null) { colnTotalCurr.Number = (colnLineTotal.Number * (1 / colnCurrRate.Number)).ToString(colnCurrencyRounding.Number).ToNumber(); } colnGrossTotalBase.Number = (colnLineTotal.Number * ((colnTotalTaxPercentage.Number / 100) + 1)).ToString(nBaseCurrRounding).ToNumber(); colnGrossTotalCurr.Number = (colnTotalCurr.Number * ((colnTotalTaxPercentage.Number / 100) + 1)).ToString(colnCurrencyRounding.Number).ToNumber(); } return 0; #endregion }
After: public SalNumber CalculateLineTotal() { #region Action using (new SalContext(this) { if (tblInvoicableLines_colnCurrRate.Number != Sys.NUMBER_Null) { tblInvoicableLines_colnTotalCurr.Number = (tblInvoicableLines_colnLineTotal.Number * (1 / tblInvoicableLines_colnCurrRate.Number)).ToString(tblInvoicableLines_colnCurrencyRounding.Number).ToNumber();* } tblInvoicableLines_colnGrossTotalBase.Number = (tblInvoicableLines_colnLineTotal.Number * ((tblInvoicableLines_colnTotalTaxPercentage.Number / 100) + 1)).ToString(nBaseCurrRounding).ToNumber(); tblInvoicableLines_colnGrossTotalCurr.Number = (tblInvoicableLines_colnTotalCurr.Number * ((tblInvoicableLines_colnTotalTaxPercentage.Number / 100) + 1)).ToString(tblInvoicableLines_colnCurrencyRounding.Number).ToNumber(); } return 0; #endregion }
Renamed references to methods
The form window that references methods (objects) in the nested
child table needs to changes as well:
Example
:Before:
privatevoid tblInvoicableLines_OnSAM_FetchRowDone(object sender, WindowActionsEventArgs e) { #region Actions e.Handled = true; Sal.SendClassMessage(Sys.SAM_FetchRowDone, Sys.wParam, Sys.lParam); this.tblInvoicableLines.CalculateLineTotal(); this.tblInvoicableLines.CalculatePricesInclTax(); // Bug 77311, start this.tblInvoicableLines.colsCompany.Text = this.dfsCompany.Text; // Bug 77311, end #endregion }
After:
privatevoid tblInvoicableLines_OnSAM_FetchRowDone(object sender, WindowActionsEventArgs e) { #region Actions e.Handled = true; Sal.SendClassMessage(Sys.SAM_FetchRowDone, Sys.wParam, Sys.lParam); this.CalculateLineTotal(); this.CalculatePricesInclTax(); // Bug 77311, start this.tblInvoicableLines_colsCompany.Text = this.dfsCompany.Text; // Bug 77311, end #endregion }
'Since the context has now changed, you need to go through each method and verify (and quite often change) the context. Basically, what is meant with “context” is the
this keyword will point to a different object now when it’s used on a form window, compared to earlier when it was used inside the nested child table.Example
:Before: public SalNumber CalculatePricesInclTax() { … using (new SalContext(this)) { … } } // “this” was earlier referencing the nexted child table
After: public SalNumber CalculatePricesInclTax() { … using (new SalContext(tblInvoicableLines)) { … } } // using “this” after having moved the code would make it reference the form window. Therefore, we need to reference the child table again.
Just like with the context change, the bind variables that earlier were used pointing to a variable/columns on the nested child table, they now point to a variable/column being located on the form window (the “frame”). These need to be changed as well
Example
:Before: public new SalString DataSourceFormatSqlIntoUser() { #region Actions using (new SalContext(this)) { return ":i_hWndFrame.frmInvoicableLinesPerOrder.tblInvoicableLines.nBaseCurrRounding"; } #endregion }
After:public new SalString DataSourceFormatSqlIntoUser() { #region Actions using (new SalContext(tblInvoicableLines)) { return ":i_hWndFrame.frmInvoicableLinesPerOrder.nBaseCurrRounding"; } #endregion }
Hint:
The
After (without the
using statement):publicnew SalString DataSourceFormatSqlIntoUser() { return ":i_hWndFrame.frmInvoicableLinesPerOrder.nBaseCurrRounding"; }
Late bind methods on a nested child table are no longer possible to
“override”. They have all been replaced with corresponding events, wrapping the
very same mechanism inside the events. So, each one of the overrides must be
replaced with a equivalent event and remapped.
Example
:Before:
#regionLate Bind Methods /// <summary> /// Virtual wrapper replacement for late-bound (..) calls. /// </summary> public override SalString vrtDataSourceFormatSqlColumnUser() { return this.DataSourceFormatSqlColumnUser(); } /// <summary> /// </summary> /// <returns></returns> public new SalString DataSourceFormatSqlColumnUser() { return "&AO.Currency_Code_API.Get_Currency_Rounding(&AO.Site_API.Get_Company(CONTRACT), &AO.Company_Finance_API.Get_Currency_Code(&AO.Site_API.Get_Company(CONTRACT)))"; }
After:
The override method is replaced with its equivalent event as shown in the picture.
#regionLate Bind Methods private void tblInvoicableLines_DataSourceFormatSqlColumnUserEvent(object sender, FndReturnEventArgsSalString e) { e.Handled = true; e.ReturnValue = tblInvoicableLines_DataSourceFormatSqlColumnUser(); } /// <summary> /// </summary> /// <returns></returns> public SalString tblInvoicableLines_DataSourceFormatSqlColumnUser() { return "&AO.Currency_Code_API.Get_Currency_Rounding(&AO.Site_API.Get_Company(CONTRACT), &AO.Company_Finance_API.Get_Currency_Code(&AO.Site_API.Get_Company(CONTRACT)))"; }
NOTE! The method that was used together with the
new keyword (DataSourceFormatSqlColumnUser ) must be renamed (e.g. tblInvoicableLines_DataSourceFormatSqlColumnUser) due to it will otherwise be treated as if it would be part of an override connected to the frame window. The new keyword should of course then be removed as well since it’s not an overridden “new” method any longer.As a result of having re-created the columns in a new object (the control
based child table), all the event handlers for the event WindowActions
that were previously connected to the old object (the nested child table) must
be reconnected.
(The event WindowActions handles all the message pumping (including PM_ &
SAM_ messages) for the child table/column).
There are two different event handler scenarios:
The WindowActions event handler for the child table
object:
This even handler is located on the main form both before and after the
refactoring of the source code. The “Serialize as Control” operation
will automatically reconnect this event handler (if there is any)
for you.
You can verify that by checking in the Events list for the child table
object.
2. The WindowActions event handler for each column on the
table object:
These event handlers are located inside the nested child table and moved
to the form level during the refactoring of the source code (as
described in step 5). Due to that, the “Serialize as Control” can’t
automatically reconnect these event handlers, hence for why they need
to be manually reconnected for each column.
Typically for both the child table object and each column is that they can only be connected to none or one event handler each.
Use the dropdown list as shown in the picture above to get an overview on the objects that needs to be reconnected. Look for everything that ends with “_WindowActions”.
Example
:Event handler tblInvoicableLines_WindowActions should connect
to the WindowActions event for the child table
tblInvoicableLines.
(This is automatically done during the “Serialize as Control”
operation.)
Event handler colsFeeCode_WindowActions should connect to the
WindowActions event for column colsFeeCode.
(Manually reconnect each one of the column events to its corresponding
WindowActions event handler.)
Verify all RMB actions for the child table. The event handlers for each menu item are normally placed on the frame and are therefore not moved from the nested class.
However, their context and especially bind variables that may have been used for database calls must be verified and possibly refactored just like the methods & event handlers.
The refactoring is now done and what remains is to test each method that was refactored. The easiest and most efficient way should be put a breakpoint in each method that was part of the nested class, ensuring it’s entered and evaluated properly when you run the application. The context and bind variables are not possible to verify in any other ways.