Converting nested child tables

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.

Remove obsolete code


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

private void 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

private void 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
}

Rename variables and methods

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 child table class contains a variable like

public
SalString sStmt = "";

The form window contains an equally named variable like
public SalString sStmt = "";

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.

Serialize the child table

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.

Move the source code

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.

Remove the old 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.

Replace references to "old" child table

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.

Compile the file

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:

private void 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:

private void 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
}

Change context

'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.

Change bind variables

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
using statement above have no meaning in this scenario and can be therefore be removed. If you’re unsure about the using statement should be there or not then just keep it.

After (without the using statement):

public new SalString DataSourceFormatSqlIntoUser()
{
   return ":i_hWndFrame.frmInvoicableLinesPerOrder.nBaseCurrRounding";
} 

Change bind variables

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:

#region Late 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.

#region Late 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.

Reconnect event handlers

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 context in menu items

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.

Verify

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.